# Animation Skill Full Context
This file concatenates the extracted lessons for long-context reads.
# Animations and AI
- Module: Animation Theory
- Lesson URL: /lessons/animation-theory/animations-and-ai/
- Markdown URL: /md/lessons/animation-theory/animations-and-ai.md
- Source mirror path: apps/anim/learn/animation-theory/animations-and-ai.html
The barriers to entry for software engineering have never been lower. Everyone can create apps thanks to AI. This means that just having a working product is no longer enough to stand out as people expect things to work. The focus has shifted to taste.
## Overview
The barriers to entry for software engineering have never been lower. Everyone can create apps thanks to AI. This means that just having a working product is no longer enough to stand out as people expect things to work. The focus has shifted to taste.
Think again about this quote from the last lesson:
Animations can be a great representation of taste. Motion is hard to get right, so great motion stands out. If it was easy, then you and 7,000+ other people probably would not have enrolled in this course, as it would just be common sense.
Because great animations are not common sense, AI struggles with them as well. An LLM is trained on lots of data, not all animations in that data set are great. And that’s good news for you, because this course is teaching you something that AI can’t do well yet.
That’s why a good understanding of motion principles will get more and more valuable. It’s a way to differentiate yourself, your brand, and your product, by showcasing your taste.
## How AI can help you
This course teaches you a set of principles that help you create great animations. We can feed those principles to an LLM and it will keep these rules in mind when helping you with animations.
I created a SKILL.md file with a set of instructions which can be used with tools like Cursor, Claude Code, OpenCode, and more.
This file tells the LLM that animations should usually be fast, what custom easings should be used, when to use bounce in spring animations, and so on. The LLM will know everything you do after going through this course.
This is especially useful if you just began your animation journey as it will constantly remind you of the rules you should follow.
## Installation
Run this command in your terminal:
```text
curl -fsSL "https://animations.dev/api/activate?email=your@email.com" | bash
```
The installer will automatically detect and set up the skill for any supported tools you have installed:
- Amp Code
- Antigravity
- Claude Code
- Cursor (1.6+)
- Gemini CLI
- OpenCode
- Windsurf
There are various ways you can use skills, you can learn about it more here.
The way I use it with Claude Code is whenever I want to use the skill, I just type /animations-dev and it will activate the skill. You should see it in the autocomplete options too upon typing / in the chat.
The skill file can review your animations, suggest improvements, and basically answer all your motion-related questions based on my experience gathered over the years.
Let me know how you like it using the feedback form at the bottom of the page.
Here’s a simple demo to give you an idea of how it works:
If you enjoy using the skill file and want to dive even deeper, there’s also a emil.md version. It’s a skill that contains all my knowledge not only about animations, but design engineering in general.
It’s meant to instantly improve the quality of your output, whether it’s design or code.
It contains everything I’ve learned from working at companies like Vercel and Linear, and building open source libraries like Sonner and Vaul.
Available for animations.dev students only.
## How do I use AI?
I use Claude Code daily when working at Linear or on my personal projects. I try to write the hard parts myself to ensure that I understand the code, but repetitive tasks, or autocompletion for simple things is something that I use a lot.
I also use DALL·E 3 and Midjourney for image generation. Great visuals make a huge difference when it comes to animations. As an example, I used Midjourney to create images in this course for the Trash interaction, App Store Cards, and Shared Layout Modals.
## Game of the day
A game about vikings
Are you ready? A game about vikings, where you can play as a viking and fight other vikings. You can also build your own viking village and explore the world.
The never ending adventureIn this game set in a fairy tale world, players embark on a quest through mystical lands filled with enchanting forests and towering mountains.
Midjourney was used to create illustrations used in this component.
You need to be careful though. With each prompt, you are not only outsourcing labor, but a bit of the thinking, the intuition, and care as well. These things can’t be outsourced. You have to be involved in the process and think deeply about the things you create in order to make something wonderful.
---
# Intro
- Module: Animation Theory
- Lesson URL: /lessons/animation-theory/intro/
- Markdown URL: /md/lessons/animation-theory/intro.md
- Source mirror path: apps/anim/learn/animation-theory/intro.html
Welcome to animations.dev!
## Overview
Welcome to animations.dev!
I know that crafting great animations can feel challenging, but that’s exactly why I built this course! You’re in good hands. By the end of it, you’ll have all the knowledge and tools to build animations that feel right.
Before we dive in, let’s go over some general information about the course.
## The structure
This course is split into four modules and a series of walkthroughs.
The first module covers the theory behind great animations. We’ll learn how to think about animations, how to choose the right easing and duration, when to animate, everything you need to stop guessing and start crafting your animations with confidence.
There’ll be lots of good vs bad examples to help you understand and feel the difference.
Then we put the theory into practice. We’ll build components like the trash interaction, a feedback popover, a 3D coin animation, and more. Each one is broken down into hands-on exercises that make the concepts stick.
One of the components we’ll build.
Then there’s a separate section called the Walkthroughs where I share my entire process with you. You’ll get to see how I create animations from scratch, including the reasoning behind my decisions.
We’ll build the Dynamic Island, Family Drawer, and a Navigation Menu.
## Lesson structure
Most of the lessons include a video and a text version with interactive components. I recommend watching the video first, and then working through the text version. It not only contains additional examples, but also includes some extra explanations.
The theory module does not contain many videos. That’s because I really want you to play with the interactive examples and truly experience what I’m trying to teach. This will be crucial in order to understand some of the concepts.
## Code Playgrounds
This course has a ton of exercises. We use an interactive code playground to allow you to write code without leaving the platform. Here’s what it looks like:
```text
import { motion } from "motion/react";
import { useState } from "react";
export default function Example() {
return (
Hello
)
}
```
Your changes are saved automatically. You can test it by changing the text to “Animations” and refreshing the page. Your edit will be preserved.
In the top right corner, you’ll find several controls. Let’s go through them:
- Reset code - Removes all your changes and resets the code to the initial state.
- Toggle line numbers - Show or hide line numbers to the left of the code.
- Toggle fullscreen - Expands the editor to fill the screen for a better coding experience.
- Refresh preview - Reloads the preview. Useful to see an intro animation for example.
Reset code - Removes all your changes and resets the code to the initial state.
Toggle line numbers - Show or hide line numbers to the left of the code.
Toggle fullscreen - Expands the editor to fill the screen for a better coding experience.
Refresh preview - Reloads the preview. Useful to see an intro animation for example.
## Progress tracking
At the bottom of each lesson, you’ll find a “Mark as complete” button. Pressing it will mark the lesson as complete and the sidebar will update the lesson with a checkmark.
Once you’ve completed 70% of the lessons, you’ll be able to generate a certificate of completion by clicking on the account dropdown in the top right corner at the top of the page.
## How to get the most out of this course?
The best way for me to teach you how to build great animations is to challenge you. The exercises might feel difficult at times, maybe even a bit uncomfortable, but it’ll be worth it. That’s how you learn most effectively.
You can’t learn how to craft great animations by watching me do it, just like you can’t learn how to play guitar by watching someone else play.
Just showing you the solution would be a disservice. I want you to truly learn, not just copy the code I wrote. Building things yourself is the best way to do that.
By following this approach, you’ll not only know how to build a Dynamic Island that looks and feels great, you’ll be able to build anything that looks and feels great.
That’s how I’d recommend you approach this course. But if you’d rather watch me solve the challenge first, or do it together with me, that’s fine too. Whatever works best for you!
## Bonus features
In addition to the lessons, I’ve created the Vault, a list of resources related to animations, design, and engineering that I personally use and recommend.
There’s also a series of interviews with great designers and engineers from companies like Vercel, Family, and Fey.
- Dennis BrotzkyEngineer at FeyThis interview is about Dennis’ background, but also about Fey, how they maintain a high level of quality, and more.
- Lochie AxonDesign Engineer at FamilyWe talk about how Lochie built Family’s site, how he got into design engineering, CSS vs Framer Motion animations, and more.
- Henry HeffernanDesign Engineer at VercelThis interview is about design engineering at Vercel, how Henry thinks about easings, taste, and more.
- Mariana CastilhoProduct Designer, poolsideWe are going to talk about product design, how she learned to code as a designer, and how that influenced her design proces
This interview is about Dennis’ background, but also about Fey, how they maintain a high level of quality, and more.
We talk about how Lochie built Family’s site, how he got into design engineering, CSS vs Framer Motion animations, and more.
This interview is about design engineering at Vercel, how Henry thinks about easings, taste, and more.
We are going to talk about product design, how she learned to code as a designer, and how that influenced her design proces
---
# Practical Animation Tips
- Module: Animation Theory
- Lesson URL: /lessons/animation-theory/practical-animation-tips/
- Markdown URL: /md/lessons/animation-theory/practical-animation-tips.md
- Source mirror path: apps/anim/learn/animation-theory/practical-animation-tips.html
This lesson is a summary of what we learned so far in the form of practical tips, but there are also some tips we haven’t covered yet. You can treat this as a reference guide that you can come back to whenever you need to make a decision or feel stuck on an animation.
## Overview
This lesson is a summary of what we learned so far in the form of practical tips, but there are also some tips we haven’t covered yet. You can treat this as a reference guide that you can come back to whenever you need to make a decision or feel stuck on an animation.
## Repetition
This is not the only place where I reuse some content. That’s on purpose. Repetition is persuasive. I’d rather want you to look at the same example multiple times by showing it to you more than once so you actually get it, instead of creating new examples just for the sake of it.
## Record your animations
When something about your animation feels off, but you can’t figure out what, record it and play it back frame by frame. This will not only help you see the animation in a new light, but also help you notice details that you might have missed at normal speed.
I record a ton of my own animations, but also those of others for inspiration.
## Fix shaky animations
When you animate using CSS’s transform, the element might shift by 1px at the start and end of the transition in some browsers.
Notice the slight shift at the end of the animation.
This happens because the browser tries to optimize the animation by swapping between GPU and CPU rendering. I won’t get into the details here, but GPU and CPU render things differently which can cause that 1px shift in some cases.
We can fix this problem by using will-change property which will tell the browser that we want to animate this element soon so it can optimize the animation and let the GPU handle it.
```css
.element {
will-change: transform;
}
```
```css
.element {
will-change: transform;
}
```
## Give yourself a break
Don’t code and ship your animations in one sitting. Take a break, step away from the code for a while. After you come back, you’ll notice things you might have missed, maybe you won’t like some of the decisions you made.
The greatest animations take time, not only to code, but to review and improve them.
The transitions in Sonner were done in a few days, but I kept replaying them literally every day until I published them. Who knows, maybe those additional few tweaks made Theo appreciate the animation the way he did.
## Scale your buttons
The interface should feel as if it’s listening to the user. You should aim to give the user feedback on all of their actions as soon as possible. Submitting a form should show a loading state, copy to clipboard action should show a success state.
One easy way to make your interface feel instantly more responsive is to add a subtle scale down effect when a button is pressed. A scale of 0.97 on the :active pseudo-class should do the job:
## Don’t animate from scale(0)
Elements that animate from scale(0) can make an animation feel off. Try animating from a higher initial scale instead (0.9+). It makes the movement feel more gentle, natural, and elegant.
scale(0) feels wrong because it looks like the element comes out of nowhere. A higher initial value resembles the real world more. Just like a balloon, even when deflated it has a visible shape, it never disappears completely.
## Don’t animate subsequent tooltips
Tooltips should have a slight delay before appearing to prevent accidental activation. Once a tooltip is open however, hovering over other tooltips should open them with no delay and no animation.
Radix UI and Base UI skip the delay once a tooltip is shown.
Radix and Base UI, two unstyled component libraries, skip the delay once a tooltip is shown. Base UI allows you to skip the animation as well. To do that you’ll need to target the data-instant attribute and set the transition duration to 0ms.
Here’s how the styles would look like to achieve this:
```css
.tooltip {
transition:
transform 0.125s ease-out,
opacity 0.125s ease-out;
transform-origin: var(--transform-origin);
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.97);
}
/** This takes care of disabling subsequent animations */
&[data-instant] {
transition-duration: 0ms;
}
}
```
```css
.tooltip {
transition:
transform 0.125s ease-out,
opacity 0.125s ease-out;
transform-origin: var(--transform-origin);
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.97);
}
/** This takes care of disabling subsequent animations */
&[data-instant] {
transition-duration: 0ms;
}
}
```
## Make your animations origin aware
A way to make your popovers feel better is to make them origin-aware. They should scale in from the trigger. You’ll need CSS’s transform-origin for this, but its default value is center, which is wrong in most cases.
Click on the feedback button below to open the popover and see it animate from the center, close it, and press J tap on the grey X icon to set the correct origin and open the popover again. Pressing S will slow the animation down so you can see the difference better.
Click on the X icon to toggle between aware and unaware origins, then click on the button.
Press J to toggle between aware and unaware origins, then click on the button.
Base UI and Radix UI support origin-aware animations through CSS variables. This means that applying these variables will set the correct origin automatically.
```css
.radix {
transform-origin: var(--radix-dropdown-menu-content-transform-origin);
}
.baseui {
transform-origin: var(--transform-origin);
}
```
```css
.radix {
transform-origin: var(--radix-dropdown-menu-content-transform-origin);
}
.baseui {
transform-origin: var(--transform-origin);
}
```
## Keep your animations fast
A faster-spinning spinner makes the app seem to load faster, even though the load time is the same. This improves the perceived performance.
Which one works harder to load the data?
A 180ms select animation feels more responsive than a 400ms one:
As a rule of thumb, UI animations should generally stay under 300ms.
## Don’t animate keyboard interactions
These actions may be repeated hundreds of times a day, an animation would make them feel slow, delayed, and disconnected from the user’s actions. You should never animate them.
To see it for yourself, focus on the input below and use arrow keys to navigate through the list. Notice how the highlight feels delayed compared to the keys you press. Now press (shift) and see how this interaction feels without animation.
Press shift to toggle the animation
## Be careful with adding animations to frequently used elements
A hover effect is nice, but if used multiple times a day, it would likely benefit the most from having no animation at all.
Imagine you interact with this list often during the day.
I usually don’t animate elements when I know they might be used often during the day.
You might not know yet which elements that will be in your case. The best advice here is to use your own product. Use it every day and with time you’ll notice which animations annoy you.
## Hover flicker
When you add a hover animation that changes the position of the element, it sometimes means that the cursor is not on that element anymore. This causes the element to flicker back and forth between the hover and the initial state.
You might have seen this problem on some sites before.
The solution here is to add a child to our box and change its position instead. This way hovering on the parent (.box) won’t cause it to change its position, only the child (.box-inner) will.
```tsx
import "./styles.css";
export default function SimpleTransformTransition() {
return (
);
}
```
```css
.box:hover .box-inner {
transform: translateY(-20%);
}
.box-inner {
height: 56px;
width: 56px;
background: #fad655;
border-radius: 50%;
transition: transform 200ms ease;
}
```
```css
.box:hover .box-inner {
transform: translateY(-20%);
}
.box-inner {
height: 56px;
width: 56px;
background: #fad655;
border-radius: 50%;
transition: transform 200ms ease;
}
```
Thanks to that, the space underneath our yellow box when hovered will still be part of our .box element and thus the hover state will still be active. Here’s a visualization to make it more clear. The yellow outline represents the bounds of our .box element when hovered.
The right one (our solution code) would not flicker on hover.
## Appropriate target area
Sometimes you can have a button that is visually quite small which makes it harder to tap on touch devices. The fix here is to use a ::before pseudo-element to create a larger hit area without changing the layout.
Apple recommends that interactive elements should have a minimum hit target of 44px. WCAG recommends the same size. Knowing this, and knowing that (almost) everyone uses Tailwind these days, we can create a touch-hitbox utility.
```css
@utility touch-hitbox {
position: relative;
&::before {
content: "";
position: absolute;
display: block;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
min-height: 44px;
min-width: 44px;
z-index: 9999;
}
}
```
```css
@utility touch-hitbox {
position: relative;
&::before {
content: "";
position: absolute;
display: block;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
min-height: 44px;
min-width: 44px;
z-index: 9999;
}
}
```
This positions a pseudo-element in the center of the interactive element, and ensures it’s at least 44px in size. This way, even if the button is 24px, we’ll be able to tap on it easier.
```jsx
```
```jsx
```
Thanks to Freddie for sharing this on X.
## Use ease-out for enter and exit animations
If you are animating something that is entering or exiting the screen, use ease-out. It accelerates at the beginning which gives the user a feeling of responsiveness.
The dropdown on the left below uses ease-in, which starts slow. The one on the right uses ease-out, which starts fast, making the animation feel faster too.
The duration for these dropdowns is identical, 300ms (you can inspect the code to verify this), but the ease-in one feels slower. That’s the difference easing makes.
You could decrease the duration to make ease-in work, but this easing is just not made for UI animations. It speeds up at the end, which is the opposite of what we want.
Here’s a visual representation of both curves, blue is ease-out. Notice how much faster it moves at the beginning, that’s what we want for our animations.
## Use ease-in-out for elements that are already on the screen
When something that is already on the screen needs to move, use ease-in-out. Think about the timeline component we discussed earlier. It mimics the motion of a car accelerating and decelerating, so it feels more natural.
Notice how this movement mimics a car accelerating and decelerating.
## Disable hover effects on touch devices
There’s no such thing as a hover on touch devices. When a user accidentally moves their finger over an element that has a hover effect, it will trigger that hover state, but that’s not the user’s intent 99% of the time. You should disable them to avoid confusion.
Since Tailwind v4, the hover: class applies only when the input device supports hover. If you are not using Tailwind, you can use the hover media query to disable them as well.
```css
@media (hover: hover) and (pointer: fine) {
.card:hover {
transform: scale(1.05);
}
}
```
```css
@media (hover: hover) and (pointer: fine) {
.card:hover {
transform: scale(1.05);
}
}
```
## Use custom easing curves
The built-in easing curves in CSS are usually not strong enough, which is why I almost never use them. Take a look at the example below where we compare two versions of the ease-in-out curve:
Custom easing here feels more energetic.
You can find the custom easing curves I use here. There’s also easings.co which has some great easings and visualizations as well.
## Use blur when nothing else works
If you tried a few different easings and durations for your animation and something about it still feels off, try adding a bit of filter: blur() to mask those imperfections.
Below we have a button that simply crossfades between two states, and another one that adds 2px of blur to that animation. It also uses tip #1 to scale the button down to 0.97 when pressed. Notice how much more pleasing the second button feels.
Click the button to toggle between two states.
Blur works here because it bridges the visual gap between the old and new states. Without it, you see two distinct objects, which feels less natural.
It tricks the eye into seeing a smooth transition by blending the two states together.
Notice how much more distinct the two states are without blur:
Scrub through the timeline to see the animation at different stages.
## Does this matter?
I remember the moment when I found out about each of these tips either from a colleague, a friend, or X. I remember how happy I was knowing I’ll be able to improve and push my skills further thanks to them. I hope you feel the same way about this course.
When I look at these tips, I know that some of them are absolutely game-changing, while others some people might not even notice.
Both of them are equally important.
They make the interface feel cohesive and consistent which makes it more predictable. It allows the user to focus on their task rather than the interface. They don’t have to think where to click next, because everything feels intuitive.
I think it’s actually good when details go unnoticed. That means that users use our product without any friction, a seamless experience. That’s what great interfaces are all about. Not animations, not details, not delight, but enabling the user to achieve their goals with ease.
---
# Spring animations
- Module: Animation Theory
- Lesson URL: /lessons/animation-theory/spring-animations/
- Markdown URL: /md/lessons/animation-theory/spring-animations.md
- Source mirror path: apps/anim/learn/animation-theory/spring-animations.html
We want to design motion that feels natural and familiar. While using the right type of easing already helps, these easings are made based on a curve and a duration. That means that we can’t create a perfectly natural motion, because the movement in the world around us doesn’t have a fixed duration.
## Overview
We want to design motion that feels natural and familiar. While using the right type of easing already helps, these easings are made based on a curve and a duration. That means that we can’t create a perfectly natural motion, because the movement in the world around us doesn’t have a fixed duration.
Notice how with CSS animations and transitions we always have to provide a duration.
```css
.element {
animation: scaleUp 0.3s ease-out;
}
@keyframes scaleUp {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
```
```css
.element {
animation: scaleUp 0.3s ease-out;
}
@keyframes scaleUp {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
```
Spring animations can help here as they are based on the behavior of an object attached to a spring in a physical world, so it feels more natural by definition. They also don’t have a duration, making them more fluid.
The example below (that I borrowed from Josh Comeau, because it’s amazing), shows the difference between a spring and an ease animation.
Notice how the bottom ball slows down more naturally.
Spring animations are heavily used in iOS; they’re also the default animation in SwiftUI. When I tried recreating the Dynamic Island for the web, I said that it feels like a living organism. It’s mainly because of the spring animation being applied there.
## How to craft a spring animation?
Like we said before, spring animations are created by describing the behavior of an object attached to a spring. This is done by providing values for mass, tension, and velocity.
These properties can be quite unintuitive, as there is no real object with mass or a spring with stiffness here, so I created a visualizer to help you understand how they influence the animation. This visualizer is heavily inspired by morse’s work.
I use this visualizer every time I need a custom spring animation.
Apple has come up with an alternative method for configuring springs, based solely on duration and bounce. Framer Motion allows you to define a spring animation with the same approach.
This means that we start using the duration parameter again, but here it refers to the perceptual duration, which is the time it takes for an animation to feel like it’s finished, even though there’s still some very subtle movement happening.
This allows us to create more natural movement, without sacrificing the duration parameter. It’s also easier to understand and use, as we don’t have to think about mass, tension, and velocity.
## Interruptibility
Sometimes, when an animation hasn’t yet finished, we need to redirect it. When that happens, a spring animation uses the velocity it had when it was re-targeted making the movement feel smooth and natural.
Click around in the demo below, notice how the ball changes its movement without losing the momentum. This wouldn’t be possible with a CSS animation.
When I was making Sonner, I made the mistake of using a CSS animation for the enter transition. What happened then was, if I quickly added two toasts, the first toast would jump to its new position because CSS animations are not interruptible.
Try adding toasts quickly and notice how they jump to their new positions.
## Bounce
While spring animations can have a bouncy effect, there are only a few instances in product UI where a bounce is appropriate. For a more physical feel, a slight bounce at the end of a drag gesture might make sense. Notice how when I drag it to dismiss we have a slight bounce at the end. However, if I simply press to close, there’s no bounce at all.
The above works, because while you can’t see it, the user has to drag to dismiss. A drag requires some force, you need to drag your finger over the screen, so adding a bounce makes this transition feel more natural. Just like throwing a ball against the wall.
I tend to avoid bounce in most cases, and if I do decide to use it, it’s a very small value. Generally, having no bounce at all should be your default to ensure that your transitions feel natural and elegant.
## Should you use spring animation for everything?
You might be thinking, why use easing if spring animations feel more natural? Unfortunately, creating actual spring animations in CSS is currently impossible. You can only mimic spring animations using the linear() function, but this remains just an approximation.
Bounce made with linear easing by jhey.
Libraries such as Framer Motion or React Spring can help, though their file sizes are quite large. You don’t necessarily need spring animations for simple transitions like color or opacity changes, but they can improve animations that involve more motion like the trash interaction below. It’s a trade-off that requires consideration.
Click on the pictures and then on the trash icon to see it animate.
## Vaul as example
I wanted it to feel like the iOS Sheet component, which utilizes spring animations. This would mean that I’d need to use a library like Framer Motion. However, I wanted to keep Vaul’s package size small, so using a large library wasn’t really an option. I decided to prioritize a smaller package size over a more native feel in this case.
Notice the small bounce that gets applied on iOS when you interact with snap points above? That’s not really possible with Vaul, because we don’t use spring animations.
## Spring animations in Figma
You can also use spring animations in Figma. They wrote a great article about the implementation of spring animations in their design tool. How Figma put the bounce in spring animations
## What’s next?
We’ve now covered the theory of spring animations, and even though we have also seen some examples, you might still have some questions. That’s why we will build a few components that rely heavily on spring animations soon, the Dynamic Island is one of them.
For now, I highly recommend visiting the links in the resources section as they contain a lot of valuable information about spring animations.
---
# Taste
- Module: Animation Theory
- Lesson URL: /lessons/animation-theory/taste/
- Markdown URL: /md/lessons/animation-theory/taste.md
- Source mirror path: apps/anim/learn/animation-theory/taste.html
As you go through this course, you start to understand why some animations feel right while others don’t. I show you bad and good examples and explain why the good example feels better.
## Overview
As you go through this course, you start to understand why some animations feel right while others don’t. I show you bad and good examples and explain why the good example feels better.
The more of these examples you see, the better your intuition gets. You start to recognize what separates good animations from bad ones. You improve your taste.
And taste has never been more valuable. With each month LLMs are more capable of taking care of the technical difficulties. Code is not the differentiator anymore, taste is.
This lesson is all about taste and how to improve it.
## Can you improve your taste?
Many people say that taste is just a personal preference. If that was true, then there would be no good and bad designs, just different personal preferences. Yet most people admire Apple’s beautiful products, and most agree that da Vinci was a great artist.
If taste is just personal preference, then everyone’s taste is already perfect.
What actually happens is as you improve at any craft and revisit your old work, you realize that it’s not just different - it’s worse. And you can tell why.
I teach you animations through good and bad examples, because when they are put next to each other, it’s easy to tell the difference, even if your taste is not developed yet.
Most people would agree that the correct example below feels better. Since we covered it earlier, you now know I didn’t build it based on my intuition, but rules that can be justified with The Easing Blueprint. This proves that taste is not just a personal preference, but a skill you can train.
Before we dive into how to improve your taste, let’s talk more about why it matters.
## Why does taste matter?
When the first car came out, consumers didn’t care about its color, or silhouette, because the competition was a horse. But now that cars have been commoditized, quality and details have become more important than ever.
The same applies to software. Simply shipping a working product is no longer enough, anyone can do that, especially now with AI. It’s not the differentiator anymore as people expect things to work. What makes a product stand out is the brand, design, interactions, how intuitive it is, the overall experience. Taste is the differentiator.
In a world of scarcity, we treasure tools. In a world of abundance, we treasure taste.
Companies start to realize that in order to stand out, they need to have a great product. Great product requires great intuition, and great intuition requires great taste.
But how can you actually improve your taste?
## How to get better?
You need to surround yourself with great work.
If you are a designer, look at great designs. If you are a writer, read great books. Expose yourself to great work, this way you’ll learn how greatness looks and feels like.
It comes down to trying to expose yourself to the best things that humans have done and then try to bring those things into what you’re doing.
Find people who are respected in their field. Look who they admire and build a curated list of tastemakers. Surround yourself with their work. Look at their designs, use their apps, read their books. Learning from the best is the best way to learn.
Designers should copy great designs, writers should copy great books.
Copy and re-implement work you admire until you can proudly create for yourself.
Heavy exposure to great things shapes output.
If you don’t know where to find your tastemakers, the course’s Vault should help. It contains stuff that I personally admire, so if you like my taste, it could be a good starting point.
The Vault is a highly curated list of resources.
## Think about why you like something
While developing your taste, don’t just label things as good or bad. Instead of relying on gut feelings, try to rationalize why something feels great. Analyze and understand patterns, don’t rely purely on your intuition.
If you’re a designer, don’t just use apps - study them. Why does this interaction feel good? If you’re a filmmaker, don’t just watch movies - think about why the director made the choices they did.
Have a mindset of thinking deeply about what makes something great. Go beyond the surface level. Be curious.
I love recreating good animations, the ones that everyone talks about when they are first seen. There is always a reason why everyone talks about them and I want to find the why behind it. So I record the animation and scrub through the recording.
Oftentimes the animation happens quickly and you don’t see every detail of it, but trust me, the greatest animations have a lot of details. Scrubbing through the video helps you notice and appreciate every single one.
I record a lot, even my own animations to see what I can improve.
This accelerated my learning process more than anything else. I recorded an animation and recreated it until I was satisfied with my replica. It took a long time, but I learned so much from it.
## Practice
Practice your craft. Create things. A designer should design, a writer should write. This will make you not only a good judge of taste, but also, with time, a tastemaker.
This is the most important step. You can’t learn to craft great animations by just watching someone else do it just like you can’t learn how to play guitar by just watching someone else play it. I show you my approach to animations, but you need to put in the work.
That’s why there are so many exercises in this course where you can choose an easing yourself. You’re not just learning to code - you’re training your taste.
While practicing, seek feedback from others. Good critique from the right person beats trial and error alone. The course’s Discord server is a great place to get feedback.
The things you’ll create probably won’t be good at first, but that’s a good sign. Your taste is good enough to tell that your work is not on par yet. That’s the taste gap that Ira Glass talks about here:
## Care
Taste doesn’t matter much if you don’t care about your work. The best people at any craft are that good, because they care. They go that extra mile and they don’t stop until they are truly satisfied with the result.
But if you bought this course, you probably care. Because why would you spend your time and money on something you don’t care about?
It’s not just me saying this for the sake of it, you can actually feel whether people cared about their product or not. Apple’s products are made with care, Linear is made with care. You feel it when you use these products.
What we make testifies who we are. People can sense care and can sense carelessness.
## Homework
Find an animation or interaction that you really like and describe why you like it. Be specific. Don’t just say the easing is nice or the text animation feels good, but rather why it feels good.
Write it down or share it on the course’s Discord server, don’t just do it in your head. This exercise will help you a lot, arguably more than the coding exercises further in the course.
This shouldn’t be something you do once for this homework exercise. If you are serious about animations and improving your taste, you should review great work from others a lot. I know how much it helped me, and I know it’ll do the same for you.
---
# The Easing Blueprint
- Module: Animation Theory
- Lesson URL: /lessons/animation-theory/the-easing-blueprint/
- Markdown URL: /md/lessons/animation-theory/the-easing-blueprint.md
- Source mirror path: apps/anim/learn/animation-theory/the-easing-blueprint.html
The main ingredient that influences how our animations feel is easing. It describes the rate at which something changes over a period of time. It’s the most important part of any animation. It can make a bad animation look great, and a great animation look bad.
## Overview
The main ingredient that influences how our animations feel is easing. It describes the rate at which something changes over a period of time. It’s the most important part of any animation. It can make a bad animation look great, and a great animation look bad.
You can animate the timeline component below with two different easings. Notice how much worse it feels with the incorrect one. Keep in mind this is just easing that’s changing.
Easing also plays an important role in how fast our interfaces feel. This is important, because the perception of speed is often times more important than the actual performance of your app.
A faster-spinning spinner makes the app seem to load faster, even when the load times are identical. This improves perceived performance.
Which one works harder to load the data?
This example focuses on duration, but easing can also influence the perception of speed. The dropdown on the left below uses ease-in easing, which starts slow. The one on the right uses ease-out, it starts fast, making the animation feel faster.
Click on the buttons to compare the easing speed.
The animation duration of both of these dropdowns is exactly the same, 300ms (you can inspect the code to verify this), but ease-in on the left feels much slower. Now imagine if all your UI animations were using ease-in, your app would feel way slower than it actually is.
To know when and what easing to choose, I’ve developed a system for myself. This blueprint covers every type of easing that is built into CSS and it describes when to use it. Additionally, it provides 16 custom easing curves that I use in my work.
Let’s go through each of them.
## ease-out
I use this curve the most in my UI work. It’s great for user-initiated interactions like opening a dropdown or a modal as the acceleration at the beginning gives the user a feeling of responsiveness. I apply this easing for most elements that have an enter and exit animation.
Dropdowns are a great use case for ease-out.
Giving users a feeling of responsiveness is important when it comes to building great interfaces, and ease-out helps a lot here.
A trick to make your UI even more responsive with the help of ease-out is to add a subtle scale down effect when a button is pressed. A scale of 0.97 on the :active pseudo-class with a 150ms transition should do the job.
ease-out also works great for enter animations on marketing pages. I often use it for intro animations at Linear. All the movement on the video below uses ease-out easing, just with different delays and durations. Notice how all the movement slows down towards the end.
## ease-in-out
Starts slowly, speeds up and then slows down towards the end, like the acceleration and deceleration of a car. I use it for elements that are already on the screen and need to move to a new position, or morph into a new shape.
The timeline component below is a good use case for ease-in-out, all animated elements stay on the screen, they just change in some way. This easing feels right, because we see an animation accelerate and decelerate in a natural way.
Notice how this movement mimics a car accelerating and decelerating.
The hackathon project below that I worked on during my time at Vercel uses ease-in-out as well, because we morph the current page into a smaller container. A perfect use case for ease-in-out.
The Dynamic Island would be a good use case for ease-in-out too. Apple actually uses spring animations to make it even more natural and organic, but if we were to convert it into an easing type, it would be an ease-in-out curve, because it changes its size while staying on the screen.
We’ll cover this component in a walkthrough series later in the course.
## ease-in
It’s the opposite of ease-out, it starts slowly and ends fast. That slow start can make interfaces feel sluggish and less responsive, so it should generally be avoided.
Here are the dropdowns from the beginning of this lesson as a reminder. ease-in feels much slower, even though the duration is the same.
You could just decrease the duration, but ease-in is simply not made for UI animations. It accelerates at the end, which is the opposite of what we want. It also feels less natural because of that. Our brain expects things to settle at the end of a movement.
Here’s a visual representation of both of the curves, blue one is ease-out. Notice how much faster the line gets drawn at the beginning, that’s what we want for our animations.
To sum it up: you should avoid ease-in as it makes the UI feel slow.
## linear
Since a linear animation moves at a constant speed, it should generally be avoided as it can make motions feel robotic and unnatural. You should use linear only for constant animations like a marquee.
A marquee is a good use case for linear easing type.
Or interactive elements where you need to visualize the passage of time, like “a hold to delete” interaction. Here, we want to show the user how much time is left, the only transition that makes sense is linear, because time is passing linearly.
We’ll build this component later on in the course.
A 3D coin rotation animation is a rare, but great use case for linear easing as well.
We’ll also build this component in a later lesson.
## ease
A similar curve to ease-in-out, but it’s asymmetrical, it starts faster and ends slower than an ease-in-out curve. I use this one mostly for hover effects that transition color, background-color, opacity, and so on.
The button below uses ease for both the subtle background color transition, and, the success animation. These smaller, more gentle animations often work best with ease, it’s an elegant curve that works well in such cases.
I actually used it in Sonner for the same reason. ease-out would technically be the right choice, because a toast enter and exits the screen, but ease makes the component feel more elegant which was more important for me in this case.
## Custom easings
In the resources section of this part I included a link to a set of custom easings I often use. They are sorted from the weakest to the strongest acceleration for each type of easing.
All the examples you’ve seen up until this point are actually using these custom easings, as the accelerations of the built-in ones are not strong enough.
Here you can see the difference between the built-in ease-in-out and a custom one from the blueprint.
Custom easing here feels more energetic.
The only time I personally use a built-in easing curve is for hover effects when I reach for the ease curve, which by the way is the default timing function for transitions in CSS.
## Create your own curve
You can also create your own custom easing curves by using the cubic-bezier function in CSS. This is a great way to experiment and get a better feel for how different curves work.
Or you can use a very specific easing for a very specific scenario like I did for Vaul. Vaul uses an easing type that is made purely to mimic iOS’ Sheet easing. I haven’t made it myself, it’s made by the Ionic Framework. This tiny detail gave the component a more native feel with little effort.
If you are not comfortable with easings just yet, I encourage you to reference the blueprint as much as possible. I also encourage you to try every easing from the blueprint after this lesson. This will give you a better understanding and feel of how each curve works.
---
# Timing and purpose
- Module: Animation Theory
- Lesson URL: /lessons/animation-theory/timing-and-purpose/
- Markdown URL: /md/lessons/animation-theory/timing-and-purpose.md
- Source mirror path: apps/anim/learn/animation-theory/timing-and-purpose.html
When done right, animations make an interface feel predictable, faster, and more enjoyable to use. They help you and your product stand out.
## Overview
When done right, animations make an interface feel predictable, faster, and more enjoyable to use. They help you and your product stand out.
But they can also do the opposite. They can make an interface feel unpredictable, slow, and annoying. They can even make your users lose trust in your product.
So how do you know when and how to animate to improve the experience?
Step one is making sure your animations have a purpose.
## Purposeful animations
Before you start animating, ask yourself: what’s the purpose of this animation? As an example, what’s the purpose of this marketing animation we built at Linear?
You can view the full animation on linear.app/ai.
This animation explains how Product Intelligence (Linear’s feature) works. We could have used a static asset, but the animated version helps the user understand what this feature does, straight in the initial viewport of the page.
This animation I made at Vercel does exactly the same. It explains (although in a simplified way) how v0 works. This makes the page more interesting than if it was just a static asset.
Another purposeful animation is this subtle scale down effect when pressing a button. It’s a small thing, but it helps the interface feel more alive and responsive.
Sonner’s enter animation, on the other hand, has two purposes:
- Having a toast suddenly appear would feel off, so we animate it in.
- Because it comes from and leaves in the same direction, it creates spatial consistency, making the swipe-down-to-dismiss gesture feel more intuitive.
But sometimes the purpose of an animation might just be to bring delight.
Morphing of the feedback component below helps make the experience more unique and memorable. This works as long as the user will rarely interact with it. It’ll then become a pleasant surprise, rather than a daily annoyance.
We’ll build this component later in the course.
Used multiple times a day, this component would quickly become irritating. The initial delight would fade and the animation would slow users down.
How often users will see an animation is a key factor in deciding whether to animate or not. Let’s dive deeper into it next.
## Frequency of use
I use Raycast hundreds of times a day. If it animated every time I opened it, it would be very annoying. But there’s no animation at all. That’s the optimal experience.
To see it for yourself, try to toggle the open state of the menu below by pressing J and then K. Which one feels better if used hundreds of times a day?
When I open Raycast, I have a clear goal in mind. I don’t expect to be “delighted”, I don’t need to be. I just want to do my work with no unnecessary friction.
Think about what the user wants to achieve and how often they will see an animation. A hover effect is nice, but if used multiple times a day, it would likely benefit the most from having no animation at all.
Imagine you interact with this list often during the day.
The same goes for keyboard-initiated actions. These actions may be repeated hundreds of times a day, an animation would make them feel slow, delayed, and disconnected from the user’s actions. You should never animate them.
To see it for yourself, focus on the input below and use arrow keys to navigate through the list. Notice how the highlight feels delayed compared to the keys you press. Now press (shift) and see how this interaction feels without animation.
Press shift to toggle the animation
You might like this animation at first because you use it in a course environment, which is not your daily driver. That’s why it’s important to really think and imagine how this would feel if you used it more frequently. Like Raycast for example.
## Perception of speed
Unless you are working on marketing sites, your animations have to be fast. They improve the perceived performance of your app, stay connected to user’s actions, and make the interface feel as if it’s truly listening to the user.
To give you an example, a faster-spinning spinner makes the app seem to load faster, even though the load time is the same. This improves perceived performance.
Which one works harder to load the data?
A 180ms dropdown animation feels more responsive than a 400ms one:
As a rule of thumb, UI animations should generally stay under 300ms.
Another example of the importance of speed: tooltips should have a slight delay before appearing to prevent accidental activation. Once a tooltip is open however, hovering over other tooltips should open them with no delay and no animation.
This feels faster without defeating the purpose of the initial delay.
Radix UI and Base UI skip the delay once a tooltip is shown.
## Choosing the right duration
We now know that the duration of your animation is influenced by its frequency of use, and that usually, animations should stay under 300ms. That’s a general rule that works most of the time, but there are a few other factors that might affect the duration you choose.
Let’s explore two of them.
Size of an element: The Vercel time machine I built for a hackathon uses an ease-in-out animation, with a 1s duration. The main reason for this duration is that the animated element is big, bigger elements are “heavier” and should animate slower.
Easing: Remember the dropdowns example? Their duration is exactly the same, but the left one feels slower. If we really wanted to use that one, we would need to shorten the duration.
Vaul’s enter animation duration is 500ms, way over the 300ms guideline. That’s because there’s a custom easing curve used there to try and match the iOS Sheet component. This curve is very steep at the beginning which means that using a duration of 300ms would feel faster than that.
You can see the easing curve used in Vaul below, it’s extremely steep. I wanted to mimic a spring animation which has that characteristic gentle ending. To achieve that, I had to make the duration pretty long while not losing its snappiness, so the beginning had to be steep.
This curve is `0.32, 0.72, 0, 1` if you’d like to try it.
## Marketing vs Product animations
Most of the rules above apply to both marketing and product animations, but marketing pages give you more freedom.
Marketing pages are the packaging of your product. They should create a memorable experience for the user, and animations can help achieve that.
These pages are also usually viewed less often than the product itself, so we can be more flexible with the duration of our animations.
At Linear, I work on the Web team, which means I’m one of the people in charge of our marketing pages. I’ll go through a few animation examples to give you a better understanding of how and when to animate on marketing pages.
The duration of this animation that you saw earlier is pretty long, but this page was a teaser for an upcoming release, so we wanted something unique and memorable.
Our usual pages like Careers have shorter intro animation, but we often add subtle touches to keep them special. In this case the blueprint-like notes around the logo are animated. It’s subtle, it doesn’t have to be flashy.
Notice the smaller animations around the logo.
These intro animations run only once. If you navigate from the homepage to a different page, and then navigate back, the animation won’t replay.
You can also use animations as subtle surprises. This momentum illustration, could’ve been static, but we decided to animate it. This interaction is not obvious, which is okay, it serves as a joyful surprise for those who find it.
Same goes for this graph animation. In Linear’s product, animating a graph like this would be wrong, there’s no point in doing that. But since it serves as just an illustration on one of our marketing pages, we decided to add an easter-egg interaction to it.
We will build a similar component later in the course.
Even on marketing pages we need to be careful with the amount of animations we add. If we overdo it, the user will become overwhelmed and animations lose their impact. If everything animates, then nothing stands out.
## Great interfaces
The goal is not to animate for animation’s sake, it’s to build great user interfaces. The ones that users will happily use, even on a daily basis. Sometimes this requires animations, but sometimes the best animation is no animation.
---
# What makes an animation feel right?
- Module: Animation Theory
- Lesson URL: /lessons/animation-theory/what-makes-an-animation-feel-right/
- Markdown URL: /md/lessons/animation-theory/what-makes-an-animation-feel-right.md
- Source mirror path: apps/anim/learn/animation-theory/what-makes-an-animation-feel-right.html
I’m sure you’ve seen an animation that made you think, “Wow, that feels good!”. But can you explain why it felt that way? What actually made it work? That’s what we’ll be unpacking in this lesson, why some animations feel better than others.
## Overview
I’m sure you’ve seen an animation that made you think, “Wow, that feels good!”. But can you explain why it felt that way? What actually made it work? That’s what we’ll be unpacking in this lesson, why some animations feel better than others.
## What’s the difference?
Does this animation feel good to you?
Probably not.
How about this one?
Better, right?
The second one uses an easing type that works better with this type of animation, which helps make it feel more natural.
The first one has a linear easing type, which feels robotic and unnatural as almost nothing in the world around us moves at a constant speed. It has no energy to its movement. It feels lifeless.
An animation feels right when it mirrors the physics we experience every day. It feels right when you are not surprised by the way it animates, because it feels familiar.
Next, consider these two buttons:
The right one feels better because the subtle blur fills the visual gap between the object’s states. Without it, the eye sees two distinct objects. Which feels less natural. Blur tricks the eye into seeing a smooth transition by blending the two states together.
Apple understands this really well. When you look at the Dynamic Island for example, it behaves like a living organism. The whole iOS UI feels very alive and natural as well.
We can make our animations feel more natural through careful choices: the right easing, duration, and the properties we want to animate. Each decision shapes how the animation feels. We’ll learn about all the ingredients in the upcoming lessons.
## Purpose
Just because animations can improve the user experience doesn’t mean that we have to animate everything on the screen. We have to pace animations through the experience. The more animations we add, the less valuable they become.
Think about what the user wants to achieve and how often they will see an animation. The hover below looks nice in a demo, but if the user hovers over the item fifty times a day, even at 200ms animation starts feeling sluggish and creates friction.
Imagine you interact with this list often during the day.
A common mistake people make starting their animation journey is animating too much, trying to “delight” the user. Usually, when a user is using your product, they have a specific goal in mind. They don’t expect to be delighted, they just want to achieve their goal.
That’s where adding animations can backfire. Instead of improving the experience, they make it worse.
We’ll get to it in the Timing and Purpose lesson, but for now, know that an animation feels right when you and the user are not surprised or annoyed by its existence. It feels right when it’s obvious why it exists.
For example a slight stagger of dropdown items might feel cool when you first click it, but notice how much quicker users can interact with the dropdown when there’s no animation of those items.
In the trash interaction below, we select images to delete and then we need some sort of UI to confirm the deletion. Since we’re already swapping states, we can make this moment a bit special by animating this transition.
## Taste
Great animation, just like great design, follows a set of rules. The Easing Blueprint shows you how to choose the right easing for your animations. Timing and Purpose helps you know when and how to animate.
These rules are a great starting point, but to get really good at animations, you need to develop great taste. And despite what some people think, taste is not just a personal preference, you can actually learn it.
If taste is just personal preference, then everyone’s already perfect.
When you have developed taste, you know what looks good and what doesn’t. At that point you’ll have also built a collection of references that you can use to guide your work. You’ll then be prepared for every scenario, whether it’s covered in the blueprint or not.
We’ll dive deeper into this in the taste lesson, but for now, know that animations that feel right are made by people with great taste.
## Summary
Animations feel right when:
- They feel natural.
- They have a purpose.
- They are made with taste.
The good news is that you’ll learn all of these ingredients in the upcoming lessons!
---
# Keyframe Animations
- Module: CSS Animations
- Lesson URL: /lessons/css-animations/keyframe-animations/
- Markdown URL: /md/lessons/css-animations/keyframe-animations.md
- Source mirror path: apps/anim/learn/css-animations/keyframe-animations.html
Similiar to transitions, keyframe animations offer another way to interpolate between states. Before we dive deeper, let’s first understand when you should use keyframe animations over transitions.
## Overview
Similiar to transitions, keyframe animations offer another way to interpolate between states. Before we dive deeper, let’s first understand when you should use keyframe animations over transitions.
## Keyframe animations vs transitions
I’ll simply list scenarios where one option is more suitable than the other.
## Use keyframe animations when:
- You need infinite loops (marquee, spinner)
- The animation runs automatically (intro animation on a page)
- You need multiple steps/states (pulse animation)
- Simple enter or exit transitions that don’t need to support interruptions (dialog, popup)
We’ll go in-depth on the examples later.
## Use CSS transitions when:
- User interaction triggers the change (hover, click, etc.)
- You need smooth interruption handling (Sonner)
You can see more examples of CSS transitions in the previous lesson.
## Creating keyframe animations
We define a keyframe animation using the @keyframes at-rule. It consists of a name and a set of keyframes. Each keyframe consists of a percentage and a set of CSS properties.
```css
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
```
```css
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
```
We can then use the animation on an element using the animation property. The code below means that we will interpolate the opacity from 0 to 1 over 1 second using ease as our easing.
```css
.element {
animation: fade-in 1s ease;
}
```
```css
.element {
animation: fade-in 1s ease;
}
```
The animation property is a shorthand for lots of other properties. I use animation for only the first 3 properties. That is animation-name, animation-duration, and animation-timing-function. I declare the rest of the properties separately for readability.
Let’s now cover a few use cases/properties for keyframe animations.
## Iteration count
The animation-iteration-count property defines how many times the animation should run. The default value is 1. You can set it to infinite to run the animation forever for example.
A marquee is a good use case for infinite iteration count.
When it comes to values higher than 1, but not infinite, I rarely use them. It’s usually either infinite or I just leave it as the default.
## Multiple steps
We can define multiple steps within a @keyframe. This is useful when you need to animate an element through multiple states. A very simple, but nice example is a blinking cursor.
This can be done with your @keyframes looking like this:
```css
@keyframes blink {
0% {
visibility: visible;
}
50% {
visibility: hidden;
}
100% {
visibility: visible;
}
}
```
```css
@keyframes blink {
0% {
visibility: visible;
}
50% {
visibility: hidden;
}
100% {
visibility: visible;
}
}
```
We can actually simplify the keyframes above. If keyframes are not specified for 0% and 100%, CSS automatically uses the element’s existing properties at those points. So by default, the element starts and ends with its natural visibility state (visible) in this case.
```css
@keyframes blink {
50% {
visibility: hidden;
}
}
```
```css
@keyframes blink {
50% {
visibility: hidden;
}
}
```
Usually though, animations with multiple steps can get quite complex. That’s why I often just reach for Motion in these cases. The movement of the purple ball in the video below could be done with keyframes, but I’d reach out for Motion instead in this case.
## Maintaining end state
By default, the animation resets to the initial state after it finishes. Below you can see how a keyframe animation that goes from scale(1) to scale(2) and resets back to scale(1) after the animation finishes. Take a look at the animation below to see it in action:
Often times we want to maintain the end state of our animation, think of a dialog or a popover. We can do it by adjusting the animation-fill-mode. This property describes how a CSS animation applies styles to its target before and after its execution. In our case we want to maintain the end state, so we set it to forwards.
There are also other values for animation-fill-mode like backwards or both, but forwards is definitely the most common one.
## How is this animation triggered?
I increment a state variable when the button is clicked. This variable is then used as the key prop on our box. This forces React to re-render the element and thus trigger the animation again.
backwards can be useful when we want to set the initial styles of our element to be the same as the first keyframe. This can be useful when we have a delayed enter animation. We then don’t have to adjust the initial styles of the element, we can simply change the animation-fill-mode to backwards instead. You can see the difference in this delayed animation below:
Notice that when you click on the "With backwards" button, the box is invisible initially. That’s because the first keyframe sets the opacity to 0. backwards applies this style to the element before the animation starts.
## Pausing animations
A unique feature of keyframe animations that transitions don’t have is that we can pause them. We can do this by setting the animation-play-state property to paused. I’ve never used it actually, but it’s good to keep it in mind when choosing between keyframe animations and transitions.
I use animation-direction: alternate to animate the box back and forth. Otherwise, the box would just teleport back to the start. I’m also using ease-in-out-cubic from the blueprint as the element as our box is already on the screen and just moves to a new position.
```css
/* This is how the animation looks like in code */
.box {
animation: xTranslate 2s cubic-bezier(0.645, 0.045, 0.355, 1);
animation-iteration-count: infinite;
animation-direction: alternate;
}
@keyframes xTranslate {
0% {
transform: translateX(-150px);
}
100% {
transform: translateX(150px);
}
}
```
```css
/* This is how the animation looks like in code */
.box {
animation: xTranslate 2s cubic-bezier(0.645, 0.045, 0.355, 1);
animation-iteration-count: infinite;
animation-direction: alternate;
}
@keyframes xTranslate {
0% {
transform: translateX(-150px);
}
100% {
transform: translateX(150px);
}
}
```
## Text reveal
Our first exercise will be a text reveal. Each letter in the word is shown with a slight delay. The end result will look like this:
We start with a static version, a centered . Good luck!
```text
import "./styles.css";
import { useState } from "react";
export default function TextReveal() {
const [reset, setReset] = useState(0);
return (
Animations
{/* Use this button to replay your animation */}
);
}
```
## Orbiting animation
We’ve talked about 3D transforms in the transforms lesson. Let’s now combine the newly acquired keyframe animations knowledge with 3D transforms. We’ll animate an element orbiting around another element. The end result will look like this:
Our starting point will be a a circle and another, smaller circle positioned absolutely on top of it. Your job will be to make the smaller circle orbit around the bigger one using keyframe animations and 3D transforms. Bonus points if you can make it grow and shrink depending on the distance between the viewer and the circle.
```text
import "./styles.css";
export default function Orbit() {
return (
);
}
```
An orbiting animation like the one above is not a work of art. I’m just giving you the tools here to create something beautiful. The loader I showed you earlier also uses this orbiting technique, it’s just a different design and an extra animation at the end. You just need to get creative.
A loader animation that Yann and I worked on at Linear.
## Reverse engineering
In the previous module, we talked about surrounding yourself with great work to develop taste. Remember this quote?
This can also be applied to coding. If you like an animation, you can inspect the code and see how it was done. Essentially reverse engineering it.
If the animation is done with CSS, you can literally see everything you need in the dev tools. If it’s involving JS, you won’t see the exact easing values, duration, etc. but you can still see which properties are animated and how the element is styled.
Just like with taste, it’s important to pick the right things to be inspired by. A few good places that I like to visit and see how things are implemented are:
- Vercel’s marketing pages and their design system
- Linear’s marketing pages
- Aave’s site, their docs are also full of pleasant surprises
These 4 sites are usually enough in my case. Curate a list for yourself and be very selective.
This will be a homework exercise in which you’ll reverse engineer an animation from Aave’s docs. You can find it on this page. This might look very hard at first, but you’ll find all the answers in the dev tools.
You can see how the end result should look like below.
A small tip is that the approach here is similar to our orbiting animation exercise. Good luck!
```text
import "./styles.css";
export default function Orbit() {
return (
);
}
function CoinIcon() {
return (
)
}
```
---
# The beauty of CSS animations
- Module: CSS Animations
- Lesson URL: /lessons/css-animations/the-beauty-of-css-animations/
- Markdown URL: /md/lessons/css-animations/the-beauty-of-css-animations.md
- Source mirror path: apps/anim/learn/css-animations/the-beauty-of-css-animations.html
There’s something special about CSS animations. No dependencies, no javascript, just HTML and CSS. It feels like you are writing code how it was meant to be written.
## Overview
There’s something special about CSS animations. No dependencies, no javascript, just HTML and CSS. It feels like you are writing code how it was meant to be written.
This module is about ensuring that you get the basics of CSS animations right and know when and how to use them. It covers everything you need to know to build beautiful things with CSS animations.
One of the animations we’ll build. Click on the button to see it animate.
I’ll start by showing you how I think about CSS animations. Below are my notes and thoughts gathered over the years.
## Right tool for the job
I agree that you don’t need Framer Motion for a simple hover animation or enter animations, but that’s pretty much it.
## Disclaimer
Framer Motion is now Motion for React. I’ll keep referring to it as Framer Motion in this course to avoid confusion. This will change in the future.
You bought this course not only to learn how to code animations, but also how to make them feel right. In the first module we brought up iOS a lot, the interactions there feel smooth, natural, and apps like Family are just a joy to use.
Video by Benji.
The truth is, this level of quality is impossible to achieve with plain CSS. You need javascript to achieve iOS-level animations.
CSS animations don’t support real spring animations.
They can feel cheap and pedestrian.
And the most important point is that your users don’t care if you use CSS animations.
They care about what they see, they care about their experience.
I personally like to build beautiful things. If that means I have to use an animation library, then I’m okay with that. As long as the end result is beautiful and works well across all devices and browsers.
The smoothness seen here would be hard to achieve with just CSS (it’s using Framer Motion).
You might think that using an animation library will increase the bundle size and may cause frame drops, which will degrade the experience. While increased bundle size is unavoidable, this is usually not a cause for concern. Frame drops will not be an issue if you animate things in the right way. And you are here to learn this right way.
CSS animations have their place of course, and we will cover many use cases in this module.
## Performance
Some CSS animations are hardware-accelerated, which means that the browser can offload the work to the GPU. A hardware-accelerated animation will remain smooth, no matter how busy the main thread is. If you are animating using transform in CSS, the animation will likely be hardware-accelerated.
That’s not the case for animations that involve the use of requestAnimationFrame, which Framer Motion uses. The busier the main thread is, the more frames will be dropped. You can see the difference below. The more logos you add, the laggier Framer Motion animation becomes.
This example is inspired by the Sidebar Animation Performance post by Josh Wootonn.
This might be obvious, but CSS animations don’t require any extra dependencies either. Using more dependencies won’t cause your animations to drop frames, but it will increase the bundle size, which will make your website load longer.
We will talk more about performance in the Performance lesson.
## When to use CSS animations
Use CSS animations when:
- You need a simple hover effect.
- You need to animate an element in or out.
- You have an infinite, linear animation like a marquee.
- You have a bundle-size sensitive project.
Use Framer Motion/other animation library when:
- You need to create complex animations.
- You want to make your animations feel more sophisticated.
- You want your animations to be interruptible and feel natural.
---
# The Magic of Clip Path
- Module: CSS Animations
- Lesson URL: /lessons/css-animations/the-magic-of-clip-path/
- Markdown URL: /md/lessons/css-animations/the-magic-of-clip-path.md
- Source mirror path: apps/anim/learn/css-animations/the-magic-of-clip-path.html
clip-path is often used for trimming a DOM node into specific shapes, like triangles. But what if I told you that it’s also great for animations?
## Overview
clip-path is often used for trimming a DOM node into specific shapes, like triangles. But what if I told you that it’s also great for animations?
In this article, we’ll dive into clip-path and explore some of the cool things you can do with it. Once you read it, you’ll start seeing this CSS property being used everywhere.
## The Basics
The clip-path property is used to clip an element into a specific shape. We create a clipping region with it, content outside of this region will be hidden, while content inside will be visible. This allows us to easily turn a rectangle into a circle for example.
```css
.circle {
clip-path: circle(50% at 50% 50%);
}
```
```css
.circle {
clip-path: circle(50% at 50% 50%);
}
```
This has no effect on layout meaning that an element with clip-path will occupy the same space as an element without it, just like transform.
## Positioning
We positioned our circle above using a coordinate system. It starts at the top left corner (0, 0). circle(50% at 50% 50%) means that the circle will have a border radius of 50% and will be positioned at 50% from the top and 50% from the left, which is the center of the element.
There are other values like ellipse, polygon, or even url() which allows us to use a custom SVG as the clipping path, but we are going to focus on inset as that’s what we’ll be using for all animations in this post.
The inset values define the top, right, bottom, and left offsets of a rectangle. This means that if we use inset(100%, 100%, 100%, 100%), or inset(100%) as a shortcut, we are "hiding" (clipping) the whole element. An inset of (0px 50% 0px 0px) would make the right half of the element invisible, and so on.
We now know that clip path can essentially "hide" parts of an element, this opens up a lot of possibilities for animations. Let’s start discovering them.
## Comparison Sliders
I’m sure you’ve seen those before and after sliders somewhere. There are many ways to create one, we could have two divs with overflow hidden and change their width for example, but we can also use more performant approach with clip-path.
We start by overlaying two images on top of each other. We then create a clip-path: (0 50% 0 0) that hides the right half of the top image and adjust it based on the drag position.
Visuals are coming from Raycast.
This way we get a hardware-accelerated interaction without additional DOM elements (we’d need additional element for overflow hidden in the width approach).
Knowing that we can create such comparison slider with clip-path opens the door to many other use cases. We could use it for a text mask effect for example.
We overlay two elements on top of each other again, but this time, we hide the bottom half of the dashed text with clip-path: inset(0 0 50% 0), and the top half of the solid text with clip-path: inset(50% 0 0 0). We then adjust these values based on the mouse position.
This one is technically a vertical comparison slider too, but it doesn’t look like one. It’s just a matter of creativity.
The dashed text here is a stroke applied in Figma that is then converted to SVG.
## Animating images
clip-path can also be used for an image reveal effect. We start off with a clip-path that covers the whole image so that it’s invisible, and then we animate it to reveal the image.
```css
.image-reveal {
clip-path: inset(0 0 100% 0);
animation: reveal 1s forwards cubic-bezier(0.77, 0, 0.175, 1);
}
@keyframes reveal {
to {
clip-path: inset(0 0 0 0);
}
}
```
```css
.image-reveal {
clip-path: inset(0 0 100% 0);
animation: reveal 1s forwards cubic-bezier(0.77, 0, 0.175, 1);
}
@keyframes reveal {
to {
clip-path: inset(0 0 0 0);
}
}
```
We could also do it with a height animation, but there are some benefits to using clip-path here. clip-path is hardware-accelerated, so it’s more performant than animating the height of the image. Using clip-path also prevents us from having a layout shift when the image is revealed, as the image is already there, it’s just clipped.
## Scroll animations
The image reveal effect must be triggered when the image enters the viewport; otherwise, the user will never see the image being animated. So how do we do that?
I usually use Framer Motion for animations, so I’ll show you how to do it with it. But if you are not using this library in your project already, I’d suggest you use the Intersection Observer API, as Framer Motion is quite heavy.
Framer Motion exposes a hook called useInView which returns a boolean value indicating whether the element is in the viewport or not. We can then use this value to trigger the animation.
```text
"use client";
import { useInView } from "motion/react";
import { useRef, useState } from "react";
export default function ImageRevealInner() {
const ref = useRef(null);
// Change to true only once & when at least 100px of the image is in view
const isInView = useInView(ref, { once: true, margin: "-100px" });
if (isInView && ref.current) {
ref.current.animate(
[{ clipPath: "inset(0 0 100% 0)" }, { clipPath: "inset(0 0 0 0)" }],
{
duration: 1000,
fill: "forwards",
easing: "cubic-bezier(0.77, 0, 0.175, 1)",
},
);
}
return (
<>
scroll down
>
);
}
```
I used WAAPI here instead of CSS animations to keep all animation-related logic in one place. I also added two options to the useInView hook. The once option makes sure that the animation is triggered only once, and the margin option makes sure that the animation is triggered when at least 100px of the image is in view.
## Tabs transition
I’m sure you’ve seen this one before as well. The problem here is that the active tab has a different text color than the inactive ones. Usually, people apply a transition to the text color and that kinda solves it.
This is okay-ish, but we can do better.
We can duplicate the list and change the styling of it so that it looks active (blue background, white text). We can then use clip-path to trim the duplicated list so that only the active tab in that list is visible. Then, upon clicking, we animate the clip-path value to reveal the new active tab.
This way we get a seamless transition between the tabs, and we don’t have to worry about timing the color transition, which would never be seamless anyway.
To better understand this, you can press the "Toggle clip path" button to see how it looks without clipping. Slowing it down by clicking on the button next to it will help you notice the difference in transition.
I’ve first seen this technique in one of Paco’s tweets, his profile is full of gems like this one.
You might say that not everyone is going to notice the difference, but I truly believe that small details like this add up and make the experience feel more polished. Even if they go unnoticed.
Below, you can see my implementation of this technique. Keep in mind that this code is simplified to focus on the clip-path part, the actual implementation would require more work. For example, to make it more accessible, I’d reach for Radix’s Tabs.
```text
"use client";
import { useEffect, useRef, useState } from "react";
export default function TabsClipPath() {
const [activeTab, setActiveTab] = useState(TABS[0].name);
const containerRef = useRef(null);
const activeTabElementRef = useRef(null);
useEffect(() => {
const container = containerRef.current;
if (activeTab && container) {
const activeTabElement = activeTabElementRef.current;
if (activeTabElement) {
const { offsetLeft, offsetWidth } = activeTabElement;
const clipLeft = offsetLeft;
const clipRight = offsetLeft + offsetWidth;
container.style.clipPath = `inset(0 ${Number(100 - (clipRight / container.offsetWidth) * 100).toFixed()}% 0 ${Number((clipLeft / container.offsetWidth) * 100).toFixed()}% round 17px)`;
}
}
}, [activeTab, activeTabElementRef, containerRef]);
return (
{TABS.map((tab) => (
))}
{TABS.map((tab) => (
))}
);
}
const TABS = [
{
name: "Payments",
icon: (
),
},
{
name: "Balances",
icon: (
),
},
{
name: "Customers",
icon: (
),
},
{
name: "Billing",
icon: (
),
},
];
```
## Going a step further
Back in 2021 I shared a theme animation on X. The implementation was a bit hacky, as I duplicated the whole page, but it worked for a quick prototype.
The way this works is I basically animate the clip-path of either light or dark theme to reveal the other one. I know which theme to animate, because I store the current theme in the state and change it when the user clicks the button.
The code is very similar to the image reveal effect. This is the thing with clip-path, once you understand the basics you can create many great animations with it, it’s just a matter of creativity.
While this implementation is hacky as it requires duplicating the element you want to animate, you can achieve the same effect with View Transitions API. We won’t get into that here to keep the article relatively concise.
## Clip Path is everywhere
Now that you know how to animate with clip-path, you should start seeing it being used in many places. Vercel uses it on their security page for example.
Although Tuple uses the width approach we discussed earlier, they could use clip-path here for a more performant solution.
And here’s the very same tabs component we discussed earlier being used on Stripe’s blog.
## Hold to delete
To solidify what we just learned, let’s implement a hold to delete animation as it’s a clever use case for clip-path.
The end result is above. The starting code is just a button with text and an icon, if you want to use the same colors of red, I’m using #FFDBDC for the background and #E5484D for the text.
```text
"use client";
export default function ClipPathButton() {
return (
);
}
```
---
# Transforms
- Module: CSS Animations
- Lesson URL: /lessons/css-animations/transforms/
- Markdown URL: /md/lessons/css-animations/transforms.md
- Source mirror path: apps/anim/learn/css-animations/transforms.html
Most animations that go beyond simple hover effects use the transform property. Before we dive deep into the world of writing animations in code, let’s first understand how transform works.
## Overview
Most animations that go beyond simple hover effects use the transform property. Before we dive deep into the world of writing animations in code, let’s first understand how transform works.
The CSS transform property allows us to change how a given element looks. It’s very powerful, because it’s not only about moving the element on the y or x axis. We can move it, rotate it, scale it, and translate it. This unlocks a lot of possibilities when it comes to animations.
We’ll go over the transform functions and cover some nuances related to them.
## Translation
The translate function allows us to move an element around. Positive values move an element down and to the right, negative values move up and to the left.
We can either use translate(x, y), or, if we only want to move the element on one axis, translateX(x) or translateY(y). I usually use the latter because it’s more readable.
Using translate doesn’t change its position in the document flow. This means that other elements will still be laid out as if the element hadn’t moved at all.
Unlike margin or padding, when using percentages, the element will be moved relative to its own size. Setting translateY(100%) for example, will move the element down by its own height.
Animations in Sonner are done exclusively using translateY() with percentages. Toasts can vary in height, so by using translateY(100%) I know that the toast will always move down by its own height, regardless of its size.
Vaul uses translateY() for the same reason. The drawer can vary in height, so by using translateY(100%) it’ll always be hidden before animating in. A hardcoded value of 300px would only work if the drawer was 300px tall. In general, I prefer to use percentages for all animations, even if the dimensions are hardcoded. Percentages are less error prone.
## Scale
scale allows us to resize an element. It works as a multiplier, scale(2) will make the element twice as big, scale(0.5) will make it half as big.
We can also use scaleX and scaleY to only scale the element on one axis, or just use scale with two values to scale on both axes (scale(x, y)). This is less common as it’ll usually look bad.
Unlike width and height, scaling an element scales its children as well, which is a good thing. When scaling a button on click we want its font size, icons, etc. to scale as well.
You can see this effect being used often for button presses, or for enter transitions. I also used it during my Linear work trial for the QR code button below. It scales down when you click it, and the text that comes in scales up.
A subtle scale down effect when a button is pressed is also a nice way to make your interface feel more responsive. In this case, I applied a scale(0.97) to the button’s :active pseudo-class.
Notice how different both of these button feel.
The Tailwind classes from above are just these two: transition-transform active:scale-[0.97].
If you want to do the same in plain CSS, it would look roughly like this:
```css
button {
transition: transform 150ms ease;
}
button:active {
transform: scale(0.97);
}
```
```css
button {
transition: transform 150ms ease;
}
button:active {
transform: scale(0.97);
}
```
Another cool use case for scale is a zoom effect. I built this lightbox component for Linear’s marketing pages. It simply performs a calculation that returns the scale value so that the image fits the screen. A subtle benefit of using scale here is that it scales the border radius correctly as well.
A tip for using scale() is to (almost) never animate from scale(0). Such animations just don’t look good and feel weird. Nothing in the world around us can disappear and reappear like that. Instead, you can combine an initial scale like 0.5 with opacity animation like I did for Clerk’s toast.
You almost can’t notice the initial scale, but it makes the animation feel better.
## Rotate
Used less often than the two previous functions, but as the name suggests, rotate allows us to rotate an element.
We will use this property for the trash interaction later in the course. When you select a few images and press the "Trash" button you will see the images animate into the bin. It’s a combination of translate and rotate functions.
Pure rotation with no other transform functions looks best with an ease-in-out type of easing. It feels natural, like a car that accelerates and decelerates. Do you remember this example?
Custom easing here feels more energetic.
## 3D Transforms
We can also use rotateX and rotateY in combination with transform-style: preserve-3d to rotate an element around a specific axis. This is useful for creating 3D effects. Here’s an interesting loading animation:
A loader animation that Yann and I worked on at Linear.
You have to see rotateX and rotateY like screws. If you screw a screw into a piece of wood, and then rotate the screw, the piece of wood will rotate as well. If you screw it from top and rotate it’s essentially like rotateY. Think of a revolving door.
180° is the back of the element.
rotateX works exactly the same, but in the other direction. It’s like a rotisserie chicken.
180° is the back of the element, upside down.
translateZ moves the element along the z-axis. Positive values bring the element closer to the viewer, while negative values push it farther away. You won’t see its effect unless you add perspective to the parent element though.
transform-style: preserve-3d property enables an element to position its children in 3D space, rather than flattening them into a 2D plane. We wouldn’t be able to create the 3D effect in the example above without it.
perspective defines the distance between the viewer and the z value, creating depth perception. The closer the viewer is to z0, the more pronounced the 3D effect will be. Small changes will appear huge, but as the distance increases, the changes will appear smaller.
## 3D Transforms in action
This may seem useless at first, but once you get a good grasp of it, you can create some really unique effects that seem impossible to achieve with just CSS at first sight. Like Aavara’s coin animation. It may seem like it’s a real 3D object, but this animation is actually using rotateY as well.
Once you get creative with it you can achieve some great animations. We will practice 3d transforms in the Keyframe animations chapter to understand these properties better.
## Transform origin
Every element has an anchor that transform animations are executed from. By default it’s the center of the element, but we can change it to influence how our animations are performed.
All kinds of popovers should animate from the trigger, not from the center. This makes it feel more natural as it won’t appear out of nowhere. That’s where setting the right transform-origin comes in handy for example. Radix supports this out of the box via a css variable.
## Order of transforms
The order of transforms matters. In the demo below we first apply rotate and then translateX. Try switching them around by clicking on the button to see how the order affects the animation.
## Exercise
Let’s try and apply what we’ve just learned. We’ll try and recreate Sonner’s stacking layout. This is not only a good layout for this specific toast component, but you can use it for a stack of cards, like Campsite is doing, or even for nested dialogs!
A stack of cards, campsite.com.
Unlike the Campsite example above, we will stack our cards upwards. The end result should look like this:
Our starting point is a set of 3 cards stacked on top of each other with a neat grid trick, but without the stacking effect. Your job will be to apply the correct transform values to make it look like the example above.
```text
import "./styles.css";
const LENGTH = 3;
export default function StackedComponent() {
return (
{new Array(LENGTH).fill(0).map((_, i) => (
))}
);
}
```
---
# Transitions
- Module: CSS Animations
- Lesson URL: /lessons/css-animations/transitions/
- Markdown URL: /md/lessons/css-animations/transitions.md
- Source mirror path: apps/anim/learn/css-animations/transitions.html
Elements on a site often have state. Buttons might change color when hovered, a toast might change its transform values when it enters the screen. By default, changes in CSS happen instantly.
## Overview
Elements on a site often have state. Buttons might change color when hovered, a toast might change its transform values when it enters the screen. By default, changes in CSS happen instantly.
CSS transitions allow you to interpolate between the initial and target states.
## What’s interpolation?
It’s the process of estimating unknown values that fall between known values. In the context of CSS transitions, it’s the process of calculating the values between the initial and target state.
To add a transition we need to use the transition property. It’s a shorthand property for four transition properties:
- transition-property
- transition-duration
- transition-timing-function
- transition-delay
Here’s an example of how it looks:
```css
.button {
/* Transition transform over 200ms with ease as our timing function and a delay of 100ms */
transition: transform 200ms ease 100ms;
}
```
```css
.button {
/* Transition transform over 200ms with ease as our timing function and a delay of 100ms */
transition: transform 200ms ease 100ms;
}
```
Let’s briefly clarify what they mean:
- transition-property: The property you want to transition. For example: transform, opacity, background-color, but also all to transition all properties.
- transition-duration: The time it takes for the transition to complete. For example: 200ms, 1s.
- transition-timing-function: The easing you want to apply to the transition. For example: ease, ease-in, cubic-bezier(0.19, 1, 0.22, 1).
- transition-delay: The time to wait before the transition starts. For example: 200ms, 1s.
To transition a change in the transform property we can simply add something like this:
```css
.box {
/* We can optionally add a delay after the timing-function */
transition: transform 0.2s ease;
}
```
```css
.box {
/* We can optionally add a delay after the timing-function */
transition: transform 0.2s ease;
}
```
This means that our transform will be interpolated from one state to another over 0.2s seconds with the ease easing. Hover over the box below to see it in practice going from scale(1) to scale(1.5) on hover.
Notice how when you hover and unhover before the transition finishes it smoothly transitions back to the original state. This happens because CSS transitions are interruptible. It’s important to keep this in mind as we will later compare it to CSS keyframe animations which are not interruptible.
## How I use CSS transitions
In the first module we talked about keeping our animations fast. Most of CSS transitions that I use are simple hover effects or transitions in which we move an element via the transform property. We want to keep our animations fast so usually my transitions will look like this:
```css
.box {
/* Or any other property that you want to animate */
transition: transform 0.2s ease;
}
```
```css
.box {
/* Or any other property that you want to animate */
transition: transform 0.2s ease;
}
```
ease is the default timing-function, but I’ve noticed that a lot of people think it’s linear, so I want to be clear about it and write it out.
I avoid using the all keyword. We often transition a few properties at once, like transform and opacity. Being explicit about what property we are transitioning ensures that we don’t transition any other property that might potentially change.
If you are animating a lot of properties with the same duration and easing, you can define it once with shorthand and compliment with transition-property:
```css
/* More repetition */
.button {
transition:
color 0.2s ease,
background-color 0.2s ease,
border-color 0.2s ease;
}
/* Less repetition, more consistency */
.button {
transition: 0.2s ease;
transition-property: color, background-color, border-color;
}
```
```css
/* More repetition */
.button {
transition:
color 0.2s ease,
background-color 0.2s ease,
border-color 0.2s ease;
}
/* Less repetition, more consistency */
.button {
transition: 0.2s ease;
transition-property: color, background-color, border-color;
}
```
I don’t use the shorthand for transition-delay. I have always found reading a transition like transition: transform 0.2s ease 1s a bit confusing. When I see transition-delay: 1s I know exactly what it does.
## Practice
In general, CSS transitions are quite straightforward. You specify which property you want to animate, how long it should take, the easing you want to apply, and optionally, a delay—that’s it! Let’s solidify this knowledge with a few exercises. The exercises will increase in difficulty.
## Simple Transform
The end goal is to move the box 20% upwards on hover. You can choose your own duration and easing. Here’s how the end result should look like:
Hover on the yellow ball to see the transition.
This might seem easy, but you will run into a problem if you pay enough attention to the details.
```text
import "./styles.css";
export default function SimpleTransformTransition() {
return (
);
}
```
## Card Hover
You might have seen this type of effect elsewhere on the web, it’s a pretty common one. The goal is to start with a hidden description of a project and reveal it when you hover over the card. Here’s how the end result should look like:
## Project name
Project description
The white background within the card would usually be a visual of a given project.
The starting state is a card with visible description. Your job is to hide it and reveal it with a transform only when hovered and when... I’ll leave the second pseudo-class to you.
```text
import "./styles.css";
export default function CardHover() {
return (
);
}
```
## Download Arrow
A similar exercise to the previous one, but in this one, we’ll have to animate two elements at once. The arrow moves down when you hover over the button, but there’s actually another one coming from the top at the same time.
Let’s try and recreate this effect. The starting code is a button with an arrow inside. Your job is to move the arrow down and reveal the second one when hovered. Good luck!
```text
import "./styles.css";
export default function DownloadArrow() {
return (
);
}
const ArrowDown = (
);
```
## Toast component
While definitely less common, CSS transitions can also be used for enter animations. This is especially useful when your animation can change its end state mid-way. Remember this Sonner example?
When we add a toast while the one before is still animating, it’ll shift to its new position when using CSS animations. This does not happen with CSS transitions. Again, this is not used often, but it’s definitely useful to know in case you ever need it.
Let’s try and build a toast component to see how this works in practice. It’s basically Sonner, but in its expanded mode. Here’s how the end result should look like:
The starting code is a that renders toasts inside of it. Every time we click on the "Add toast" button we increment the toasts state variable which we then use to render the toasts. This is just to simplify the code. In a real-world scenario, we’d probably have an array of toast objects.
```tsx
{Array.from({ length: toasts }).map((_, i) => (
))}
```
```tsx
{Array.from({ length: toasts }).map((_, i) => (
))}
```
This adds toasts on top of each other. Your job is to make them animate each time when a toast changes position. You can choose your own duration and easing, I’ll explain in the solution what easing I chose and why. Also, you can click on the refresh icon in the header of the playground to remove all toasts you’ve added.
```text
import "./styles.css";
import { useState } from "react";
export default function Toaster() {
const [toasts, setToasts] = useState(0);
return (
);
}
```
## Disabling hover effects
Many CSS transitions are triggered by hovering over an element. Hovering is technically only possible when using a pointer device. However, when you tap an interactive element on a touch device, it triggers the hover state as well.
This is annoying and usually accidental. To avoid this we can apply a media query that will only trigger a hover effect if a pointer (like a mouse) is used to interact with the element.
```css
@media (hover: hover) and (pointer: fine) {
.card:hover {
background: blue;
}
}
```
```css
@media (hover: hover) and (pointer: fine) {
.card:hover {
background: blue;
}
}
```
You can enable this in Tailwind as well in your tailwind config:
```js
// tailwind.config.js
module.exports = {
future: {
hoverOnlyWhenSupported: true,
},
// ...
}
```
```js
// tailwind.config.js
module.exports = {
future: {
hoverOnlyWhenSupported: true,
},
// ...
}
```
This will apply the media query above to all hover effects automatically.
It’s also the default in Tailwind v4, so you don’t even have to worry about it there.
## Hover in Tailwind v4
If you are using Tailwind v4, you don’t even have to worry about it. Hovers are disabled on touch devices by default.
---
# Morph effect
- Module: Dynamic Island
- Lesson URL: /lessons/dynamic-island/morph-effect/
- Markdown URL: /md/lessons/dynamic-island/morph-effect.md
- Source mirror path: apps/anim/learn/dynamic-island/morph-effect.html
This is our final piece of this component. Let’s first look at the morphing effect from Apple’s Dynamic Island again. A small disclaimer—I don’t know Swift, so I don’t know how Apple implemented it. I’ll talk about it from a Framer Motion perspective.
## Overview
This is our final piece of this component. Let’s first look at the morphing effect from Apple’s Dynamic Island again. A small disclaimer—I don’t know Swift, so I don’t know how Apple implemented it. I’ll talk about it from a Framer Motion perspective.
## Apple’s approach
Most of the time, the Dynamic Island transitions between its idle state and an activity, like a phone call. This is easier to implement because it transitions from a black background to a rich view rather than between two UI-rich states. You can see this in the video below
It becomes tricky when morphing between two rich states. Apple avoids showing both states simultaneously by using a method similar to ‘wait mode’ in AnimatePresence from Framer Motion. The exiting state disappears before the entering state starts to animate in.
Animating with popLayout would introduce a lot of issues, because we animate these views inside a container that not only changes its height, but also its width. Because a lot of stuff in Framer Motion is happening magically under the hood, we lose control of how the views are animated.
One of the issues with popLayout in this specific case.
Framer Motion doesn’t know that we want our animation to mimic those in Dynamic Island. It simply calculates the layout changes and animates them using a predefined formula.
Now we could obviously use the wait mode on AnimatePresence, but I really wanted to make a crossfade-like transition, and the fact that it was harder to implement made it even more interesting. I also just think that this effect is more satisfying.
## The solution
I basically duplicated the active view. One version is responsible for only the enter animation, while the other is hidden and only shows up when it’s exiting. That allows us to show both views, at the same time, without having to fight against Framer Motion as one of them is positioned absolutely.
```jsx
// This is the active view which is always visible
{content}
// This shows only when exiting
{content}
```
```jsx
// This is the active view which is always visible
{content}
// This shows only when exiting
{content}
```
The way I show the second view only when it’s exiting is by using keyframes. The initial opacity is 0, and I only change it in the exit animation using variants like this:
```jsx
const variants = {
exit: (transition) => {
return {
...transition,
opacity: [1, 0],
filter: "blur(4px)",
};
},
};
```
```jsx
const variants = {
exit: (transition) => {
return {
...transition,
opacity: [1, 0],
filter: "blur(4px)",
};
},
};
```
We start with opacity 1, which makes it visible again.
## Custom exit animation
Each view can’t have the same animation, because it varies in size. When transitioning from a small view to a larger one, exiting items should scale up. At the same time, when transitioning from a large view to a smaller one, exiting items should scale down to fit the shrinking Island.
When I was working on these lessons I built a function to calculate the scale, scaleX, and y values. It’s right below in case you are curious.
```jsx
// This was the function I came up with
function calculateScale(width, lastWidth, height, lastHeight) {
// Adjust the scaling factor to account for the differences in widths
const scaleFactor = Math.pow(width / lastWidth, 0.4);
const scale = Math.round(scaleFactor * 10) / 10; // Rounding to nearest tenth for scale consistency
// Fine-tune y calculation
const y = Math.round((height - lastHeight) * 0.23 * 10) / 10; // Adjusted scaling factor to 0.1, rounded to nearest tenth
// If the island's width is smaller, push the exiting view to the inside
const scaleX = lastWidth > width ? 0.9 : 1;
return {
scaleX,
scale,
y,
};
}
```
```jsx
// This was the function I came up with
function calculateScale(width, lastWidth, height, lastHeight) {
// Adjust the scaling factor to account for the differences in widths
const scaleFactor = Math.pow(width / lastWidth, 0.4);
const scale = Math.round(scaleFactor * 10) / 10; // Rounding to nearest tenth for scale consistency
// Fine-tune y calculation
const y = Math.round((height - lastHeight) * 0.23 * 10) / 10; // Adjusted scaling factor to 0.1, rounded to nearest tenth
// If the island's width is smaller, push the exiting view to the inside
const scaleX = lastWidth > width ? 0.9 : 1;
return {
scaleX,
scale,
y,
};
}
```
But then I thought that this is not how I built it initially. I actually hardcoded the values for each transition to have granular control to be able to fine-tune it. While this approach is less flexible, to me a Dynamic Island on the web has a finite number of states, so you won’t need to create transitions for hundreds of views.
After some testing I came up with the following values for the transitions:
```jsx
const ANIMATION_VARIANTS = {
"ring-idle": {
scale: 0.9,
scaleX: 0.9,
bounce: 0.5,
},
"timer-ring": {
scale: 0.7,
y: -7.5,
bounce: 0.35,
},
"ring-timer": {
scale: 1.4,
y: 7.5,
bounce: 0.35,
},
"timer-idle": {
scale: 0.7,
y: -7.5,
bounce: 0.3,
},
};
```
```jsx
const ANIMATION_VARIANTS = {
"ring-idle": {
scale: 0.9,
scaleX: 0.9,
bounce: 0.5,
},
"timer-ring": {
scale: 0.7,
y: -7.5,
bounce: 0.35,
},
"ring-timer": {
scale: 1.4,
y: 7.5,
bounce: 0.35,
},
"timer-idle": {
scale: 0.7,
y: -7.5,
bounce: 0.3,
},
};
```
This approach covers every possible transition. We scale up or down based on dimensions, knowing, for example, that a timer is bigger than the idle state, so we scale the exiting items up accordingly. scaleX is needed to push the exiting items to the center of the Island if it gets narrower. Otherwise, they would bleed out of the Island at a certain pointT
## Custom bounce
The amount of bounce also needs to be adjusted based on the active view. Smaller views require more bounce to be noticeable. If we used the same bounce amount for all animations, smaller views would look fine, but larger views would appear faster since they need more time to settle.
I took the same approach here—hardcoded bounce values based on the current variant.
```jsx
const BOUNCE_VARIANTS = {
idle: 0.5,
"ring-idle": 0.5,
"timer-ring": 0.35,
"ring-timer": 0.35,
"timer-idle": 0.3,
"idle-timer": 0.3,
"idle-ring": 0.5,
};
```
```jsx
const BOUNCE_VARIANTS = {
idle: 0.5,
"ring-idle": 0.5,
"timer-ring": 0.35,
"ring-timer": 0.35,
"timer-idle": 0.3,
"idle-timer": 0.3,
"idle-ring": 0.5,
};
```
Every time the active view changes we are modifying the variantKey state variable with which we can retrieve the correct animation values. The bounce value is assigned inline, and the animation value is passed through the custom prop of AnimatePresence which we covered earlier.
```jsx
// ...
```
```jsx
// ...
```
## The result
Everything combined gives us this beautiful, Apple-like result. You might have expected a more sophisticated solution, but I think it’s worth showing that sometimes solutions like this one also give you the expected results. I do this often when I’m prototyping something to save time.
The demo in the sandpack below might have a bugged border-radius when you switch to the timer view for the first time. This happens in the sandpack editor only and is not an issue if you copy and paste it into your own project. I’m working on a fix for this.
```text
"use client";
import { useMemo, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import { Ring } from "./ring";
import { Timer } from "./timer";
export default function DynamicIsland() {
const [view, setView] = useState("idle");
const [variantKey, setVariantKey] = useState("idle");
const content = useMemo(() => {
switch (view) {
case "ring":
return ;
case "timer":
return ;
case "idle":
return ;
}
}, [view]);
return (
{content}
{content}
{["idle", "ring", "timer"].map((v) => (
))}
);
}
const variants = {
exit: (transition) => {
return {
...transition,
opacity: [1, 0],
filter: "blur(5px)",
};
},
};
const ANIMATION_VARIANTS = {
"ring-idle": {
scale: 0.9,
scaleX: 0.9,
bounce: 0.5,
},
"timer-ring": {
scale: 0.7,
y: -7.5,
bounce: 0.35,
},
"ring-timer": {
scale: 1.4,
y: 7.5,
bounce: 0.35,
},
"timer-idle": {
scale: 0.7,
y: -7.5,
bounce: 0.3,
},
};
const BOUNCE_VARIANTS = {
idle: 0.5,
"ring-idle": 0.5,
"timer-ring": 0.35,
"ring-timer": 0.35,
"timer-idle": 0.3,
"idle-timer": 0.3,
"idle-ring": 0.5,
};
```
---
# Ring view
- Module: Dynamic Island
- Lesson URL: /lessons/dynamic-island/ring-view/
- Markdown URL: /md/lessons/dynamic-island/ring-view.md
- Source mirror path: apps/anim/learn/dynamic-island/ring-view.html
Our approach to building the Dynamic Island will differ from previous exercises. Instead of step-by-step exercises, I’ll show you how I built each piece. This component is quite unusual, and some solutions are equally unconventional.
## Overview
Our approach to building the Dynamic Island will differ from previous exercises. Instead of step-by-step exercises, I’ll show you how I built each piece. This component is quite unusual, and some solutions are equally unconventional.
You are, of course, welcome to try and build each piece yourself, but I have to say that this component is more challenging than the previous ones.
## Starter code
We’ll start by animating our views. The first one will be the "ring". Before we jump into it, let’s first walk through our starter code.
We change the active view through two buttons that set the view state to either "ring" or "idle". The current view is then rendered via a useMemo hook, the same way as we did for the Family Drawer.
```jsx
const content = useMemo(() => {
switch (view) {
case "ring":
return ;
case "idle":
return ;
}
}, [view]);
```
```jsx
const content = useMemo(() => {
switch (view) {
case "ring":
return ;
case "idle":
return ;
}
}, [view]);
```
When it comes to the ring component we basically render a few things conditionally and slightly adjust the styling based on the isSilent value which changes automatically every 2 seconds through a useEffect hook.
```text
"use client";
import { useMemo, useState } from "react";
import { Ring } from "./ring";
export default function DynamicIslandStarter() {
const [view, setView] = useState("idle");
const content = useMemo(() => {
switch (view) {
case "ring":
return ;
case "idle":
return ;
}
}, [view]);
return (
{content}
);
}
```
## What do we animate?
Our point of reference will be the video below. Before we jump into the code, let’s analyze it and see what we’ll need to do.
When switching to "Silent" mode, the Island slightly enlarges. A red background appears behind the bell icon, expanding from the left. A line is drawn across the icon, and the bell shakes slightly during the animation. Timing is crucial here, it has to feel as if the bell "pops" into it’s new state.
The text on the right is simple, but satisfying. It’s a combination of scale, blur, and opacity transition.
## Animating width
Let’s start by animating our Island, which is the core of our component. In this case we don’t animate between auto values like we did in Family Drawer’s case. The values here are fixed, because we want full control over the animation to ensure that it feels right.
With that being said, we can simply turn our wrapper div in our ring component into a motion.div and animate the width depending on the isSilent value. We can also remove the width classes and clsx function altogether, as we only need one string as the class name.
```jsx
...
```
```jsx
...
```
This works, but the animation will feel a bit lifeless. We talked about adding bounce, so let’s add it with a value of 0.5. It works for now, it’s not perfect, but it’ll get better once we add the crossfade-like animation between views later.
If we now go back to the previous view, there’s no width transition, and that’s because we are missing the layout prop on our Island. This looks a lot better now, but there is one small issue, and I’m curious whether you can spot it.
There is a small border radius distortion happening. That’s because layout animations are done with transform, which can distort properties like border-radius and box-shadow. Framer Motion is able to fix such distortions, but only if our border radius value is in pixels. Currently, we use tailwind, which defaults to rem values. Let’s convert our border radius to pixels using inline styles.
```jsx
{content}
```
```jsx
{content}
```
## Animating text
We need to use AnimatePresence with popLayout mode here. Let’s not forget the key prop for each text node. We should also change the transform origin for the "Ring" text to right, otherwise, it would exit to the left, which is the opposite of what we want.
```text
"use client";
import { useMemo, useState } from "react";
import { Ring } from "./ring";
import { motion } from "motion/react";
export default function DynamicIslandStarter() {
const [view, setView] = useState("idle");
const content = useMemo(() => {
switch (view) {
case "ring":
return ;
case "idle":
return ;
}
}, [view]);
return (
{content}
);
}
```
## Animating the bell
The bell animation is a bit more complex as it consists of multiple smaller animations. Let’s start with the red background. There are multiple ways to do this, you can use scale, scaleX, but after some experimentation I decided to use width. We’ll go from 0 to 40px so that our bell icon is in the center of the background.
This red background is positioned absolutely, so the performance impact of us animating its width is not that big.
We’ll also animate opacity and blur. Most of the elements in the Dynamic Island have a slight blur transition, which fits the overall design and vibe perfectly.
The combination of these three properties and a spring animation with 0.35 bounce gives us the following effect.
Now, all that is left is our shake animation. Whenever I have to craft a shake-like animation I reach for keyframes. Fortunately, Framer Motion has keyframes as well.
We are looking to slightly adjust the rotate property going from negative to positive each time. This way it goes from left to right and back, each time with a smaller angle. The keyframes look as follows:
```jsx
[0, 20, -15, 12.5, -10, 10, -7.5, 7.5, -5, 5, 0]
```
```jsx
[0, 20, -15, 12.5, -10, 10, -7.5, 7.5, -5, 5, 0]
```
This is exactly what we want! Last thing we can do here is change the X position of our bell so that it is centered in the background. A change in the x property by 9px will be our sweet spot.
```jsx
...
```
```jsx
...
```
Because we added the rotate animation to the wrapping div rather than the bell svg we get a free shake animation on our line that goes across the bell. This is exactly what we want, except we want that line to be hidden when our view is not in silent mode.
We want some sort of "draw" animation for it. We can do that by animating the height property from 0 to, in our case, 16px. I also played around with the transition prop so that the easing, duration and delay play well together with the rest of the animations.
It might look like a lot for a pretty small component, but all of our work here gives us a very satisfying result.
```text
"use client";
import { useMemo, useState } from "react";
import { Ring } from "./ring";
import { motion } from "motion/react";
export default function DynamicIslandStarter() {
const [view, setView] = useState("idle");
const content = useMemo(() => {
switch (view) {
case "ring":
return ;
case "idle":
return ;
}
}, [view]);
return (
{content}
);
}
```
## A small detail
If you inspect Apple’s animation closely, you can see that the clapper (the thing inside the bell, I’ve looked it up) is moving independently from the bell. This makes the whole animation even more natural. I haven’t implemented it, but it could be a nice homework exercise for you!
---
# The Design
- Module: Dynamic Island
- Lesson URL: /lessons/dynamic-island/the-design/
- Markdown URL: /md/lessons/dynamic-island/the-design.md
- Source mirror path: apps/anim/learn/dynamic-island/the-design.html
The Dynamic Island will be our most challenging component in this course. At least it was the hardest one for me when I initially built it. Before we start, let’s study the design and understand why and how Apple made it.
## Overview
The Dynamic Island will be our most challenging component in this course. At least it was the hardest one for me when I initially built it. Before we start, let’s study the design and understand why and how Apple made it.
## Studying the design
When I first started working on this component, I didn’t have the latest iPhone and therefore no Dynamic Island. Luckily, Apple uploaded a video called Design Dynamic Live Activities, which was filled with lots Dynamic Island examples.
This talk is also a great resource for understanding how Apple thinks about the Dynamic Island. This quote stood out to me when I first watched it:
It indicates that we’ll need to use spring animations with bounce to give it an elastic, natural feel. Understanding the motion behind the Dynamic Island is crucial. If we mess up the spring animation, we’ll lose the illusion of a living organism, which is key here.
## Video reference
I recorded a video reference for myself to easily view and compare my animations. This is essential for recreating any component. You need to watch it repeatedly, basically to the point where you are sick of it. Rewatch it every time you make a change and compare it with the original to see if your changes are moving in the right direction.
## Morphing
The way the Dynamic Island transitions between views is interesting. They don’t crossfade but wait until the old view is animated out before animating the new view in. Animating both simultaneously is definitely harder, at least with Framer Motion.
I think crossfade-like transition looks better, so we’ll build it that way, even though it’s harder.
The exiting and entering items are also scaled down or up, depending on the size change of the Island. This tells me that a simple layout animation from Framer Motion won’t give us enough control. I didn’t know what would work when I first analyzed it for myself, but we’ll figure it out together.
## Views
We’ll use two views of the Dynamic Island: ‘ring’ and ‘timer’. This will serve as an opportunity to practice what we’ve learned so far and explore some new Framer Motion features as well. You can see the final version of our component below.
There are also tons of other views that you could build. It would be a great way to practice your craft, as these views are beautifully designed and animated.
I highly recommend watching the "Design dynamic Live Activities" talk. You’ll see more great UI examples there and understand how Apple thinks about the Dynamic Island.
---
# Timer view
- Module: Dynamic Island
- Lesson URL: /lessons/dynamic-island/timer-view/
- Markdown URL: /md/lessons/dynamic-island/timer-view.md
- Source mirror path: apps/anim/learn/dynamic-island/timer-view.html
This view is less complicated than the ring view. It has one constant animation and 1 button animation on the left in which we animate the pause button into a play button.
## Overview
This view is less complicated than the ring view. It has one constant animation and 1 button animation on the left in which we animate the pause button into a play button.
Our starting point will be this static, working version. The countdown uses a setInterval function inside a useEffect to go from 60 to 0, then it resets the timer. One thing worth mentioning is that we use tabular-nums for our numbers, which keeps them monospaced and their sizes consistent. Otherwise, they would slightly shift each time the number changes.
```text
"use client";
import { useMemo, useState } from "react";
import { Timer } from "./timer";
export default function DynamicIsland() {
const [view, setView] = useState("timer");
const content = useMemo(() => {
switch (view) {
case "ring":
return ;
case "timer":
return ;
case "idle":
return ;
}
}, [view]);
return (
{content}
);
}
```
## Countdown animation
This is a great use case for AnimatePresence, as each number is rendered separately with a unique key. Let’s wrap our countArray with AnimatePresence and set initial={false} and mode="popLayout". We don’t want to animate on the initial render, and we want the elements to exit and enter simultaneously, which is why we use the popLayout mode.
When it comes to the animation itself, we want the numbers to come from the bottom and disappear to the top, both with a slight blur and bounce.
```jsx
{n}
```
```jsx
{n}
```
The combination of AnimatePresence and our animation gives us the following result:
## Play button animation
The only thing left now is the button animation. You might have seen a very similar animation before in this course. Do you remember it?
This uses the wait mode in AnimatePresence, which is what we want here as well. We want to wait until the play button is gone before we show the pause button.
We’ll wrap our ternary that decides which icon to render with AnimatePresence, add initial={false} and mode="wait" to it. For the animation itself, we want to animate the scale, opacity, and blur. After some experimentation I came up with the following values:
```jsx
...
```
```jsx
...
```
In my opinion, scaling down all the way to 0 doesn’t look good in this type of animation, so I default to 0.5. The blur value is consistent with other Dynamic Island blur animations. Animating higher blur values creates a big spread, which is usually undesirable. Additionally, larger blur values can negatively impact performance, especially in Safari. A duration of 0.1 seconds strikes a nice balance between speed and smoothness.
I’ve also added a whileTap prop to our button. This prop defines an animation target when the element is pressed, providing a nice responsive feel.
```text
"use client";
import { useMemo, useState } from "react";
import { Timer } from "./timer";
export default function DynamicIsland() {
const [view, setView] = useState("timer");
const content = useMemo(() => {
switch (view) {
case "ring":
return ;
case "timer":
return ;
case "idle":
return ;
}
}, [view]);
return (
{content}
);
}
```
---
# Crossfade
- Module: Family Drawer
- Lesson URL: /lessons/family-drawer/crossfade/
- Markdown URL: /md/lessons/family-drawer/crossfade.md
- Source mirror path: apps/anim/learn/family-drawer/crossfade.html
I’ve updated the UI of the drawer between this lesson and the last one. This is a course about animations, so we won’t focus too much on the styling, but let’s go through it so you know what changed.
## Overview
I’ve updated the UI of the drawer between this lesson and the last one. This is a course about animations, so we won’t focus too much on the styling, but let’s go through it so you know what changed.
I’ve split the component into a few files so that it’s easier to work on the exercises later. We now have a components file that contains all the views.
These views are then used in our useMemo hook. useMemo is usually my preferred approach for conditional rendering with multiple possible outcomes. If it were just two components, I would use a ternary.
```jsx
const content = useMemo(() => {
switch (view) {
case "default":
return ;
case "remove":
return ;
case "phrase":
return ;
case "key":
return ;
}
}, [view]);
```
```jsx
const content = useMemo(() => {
switch (view) {
case "default":
return ;
case "remove":
return ;
case "phrase":
return ;
case "key":
return ;
}
}, [view]);
```
I use a lot of arbitrary values in Tailwind. For example, instead of mt-5, I use mt-[21px]. I usually try to avoid this, but in this case, Benji shared Family’s Figma file with me, and I wanted to ensure the spacing is as close as possible to the original.
clsx is a library I use pretty often, it is used to conditionally add classes to an element. You could also create your own utility function, but this one also takes care of objects and doesn’t weigh much.
```jsx
```
```jsx
```
Or when I need to add a class conditionally when the state of the component changes for example.
```jsx
```
```jsx
```
The buttons now have a small scale transition on press, we go from 1 to 0.95 with the default transition settings from tailwind.
Last thing is the font. Family uses a custom font in their app, it’s a sans-serif font that is a bit rounded. The closest, free font I could find was Open Runde. It’s a rounded variant of Inter. This is important as in recreations like this one it’s not only about the animations, it has to look like the original as well.
In the code editor here I assign the font inline, that’s a limitation of Sandpack, which I use under the hood. I don’t usually inline this type of styles.
## Crossfade
By updating the styles we get a lot closer to the actual Family iOS drawer. Let’s now add the crossfade animation to it. This is how the end result should look like::
```text
"use client";
import { useMemo, useState } from "react";
import { Drawer } from "vaul";
import useMeasure from "react-use-measure";
import { motion, AnimatePresence } from "motion/react";
import { DefaultView, Key, Phrase, RemoveWallet } from "./components";
import { CloseIcon } from "./icons";
export default function FamilyDrawer() {
const [isOpen, setIsOpen] = useState(false);
const [view, setView] = useState("default");
const [elementRef, bounds] = useMeasure();
const content = useMemo(() => {
switch (view) {
case "default":
return ;
case "remove":
return ;
case "phrase":
return ;
case "key":
return ;
}
}, [view]);
return (
<>
setIsOpen(false)}
/>
{content}
>
);
}
```
---
# First animations
- Module: Family Drawer
- Lesson URL: /lessons/family-drawer/first-animations/
- Markdown URL: /md/lessons/family-drawer/first-animations.md
- Source mirror path: apps/anim/learn/family-drawer/first-animations.html
This component might look a bit overwhelming at first, that’s why we’ll break it down into smaller parts. I’ve also done it when I first built it.
## Overview
This component might look a bit overwhelming at first, that’s why we’ll break it down into smaller parts. I’ve also done it when I first built it.
## Using existing solutions
Our starting point is the component below. Upon opening, you’ll see the drawer appear instantly. It’s not draggable, you can’t close it with the Escape key, and focus is not trapped within the drawer, which is an accessibility issue.
We could either code these features ourselves, or use an existing solution. While enter and exit animation and focus trapping are relatively easy to implement, dragging is a bit more complex. Especially if we want to do it right, meaning momentum-based dragging, matching overlay’s opacity with the drag progress, and more. It’s time-consuming and we already have a lot on our plate.
I built an open-source drawer component in 2023 called Vaul. It mimics the iOS Sheet component and, in my opinion, is perfect for our use case. Let’s implement it. You can read the basic documentation and see examples here.
Although this part of the build process is not strictly related to animations, I believe learning how to integrate a third-party library into your projects can be valuable.
## Tip
If you are using Vaul version 1.1.0 or higher, you can use the --initial-transform variable to adjust the animation offset which is useful in this case as the drawer doesn’t touch the bottom. You can read more about it here.
```text
"use client";
import { useState } from "react";
import { Drawer } from 'vaul';
export default function FamilyDrawer() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
{isOpen ? (
<>
setIsOpen(false)}
/>
>
) : null}
>
);
}
```
This way, we get basic animations and natural dragging behavior for free. We can now focus on the more complex parts of the component.
## Height animation
I’ve added one state variable called view, which is responsible for rendering the right content. In this demo, we’ll have 4 views: default, remove, phrase, and key. For now, these views contain text of various lengths so that we get a difference in height, we’ll add actual content later.
Clicking on different buttons will change the view, but the height changes instantly, this is obviously not what we want. Let’s animate it!
```text
"use client";
import { useMemo, useState } from "react";
import { Drawer } from "vaul";
import useMeasure from "react-use-measure";
import { motion } from "motion/react";
export default function FamilyDrawer() {
const [isOpen, setIsOpen] = useState(false);
const [view, setView] = useState("default");
const content = useMemo(() => {
switch (view) {
case "default":
return (
This is the default case
);
case "remove":
return (
You haven’t backed up your wallet yet. If you remove it, you could
lose access forever. We suggest tapping and backing up your wallet
first with a valid recovery method.
);
case "phrase":
return (
Keep your Secret Phrase safe. Don’t share it with anyone else. If
you lose it, we can’t recover it.
);
case "key":
return (
Your Private Key is the key used to back up your wallet. Keep it
secret and secure at all times.
);
}
}, [view]);
return (
<>
setIsOpen(false)}
/>
{content}
>
);
}
```
In the next part, we’ll focus on the cross fade animation.
---
# The analysis
- Module: Family Drawer
- Lesson URL: /lessons/family-drawer/the-analysis/
- Markdown URL: /md/lessons/family-drawer/the-analysis.md
- Source mirror path: apps/anim/learn/family-drawer/the-analysis.html
One thing that really helped me learn animations was recreating some of the best ones. The key to a good recreation is figuring out why an animation feels right—like which properties are being animated, what kind of easing or spring is used, and so on.
## Overview
One thing that really helped me learn animations was recreating some of the best ones. The key to a good recreation is figuring out why an animation feels right—like which properties are being animated, what kind of easing or spring is used, and so on.
## Recordings
Most animations are fast, so it’s tough to catch all the details. What works best for me is recording the animation and playing it back frame by frame or in slow motion. Since Family is an iOS app, I just use my phone to record the interaction I want to recreate, then send it to my Mac to replay.
Best view to inspect the animation is the settings screen shown above.
There are a few things that stick out to me even before we analyze it frame by frame:
- The interactions feel fast, probably an ease-out easing or a spring animation with no bounce.
- The animation likely involves only opacity and height changes.
- The drawer is draggable, so we can use Vaul here.
Let’s slow it down and see what’s happening frame by frame.
We can now see a subtle crossfade between the new and old content. To make this work, we’ll probably need to use the popLayout mode on AnimatePresence.
The content follows the height of the drawer. If the drawer gets taller, the exiting content moves up too. The duration of the animation is the same for both entering and exiting content. It’s pretty fast, so without slow motion, the fade-out is almost unnoticeable.
One thing that stood out to me is the button animation. It’s really satisfying to press it and see the transition right after. The bottom action buttons also have a different transition than the rest of the content. Their y-translate is smaller, and they seem to scale down a bit.
Everything we’ve talked about so far gives the animation a certain feel. It feels light, fast, and satisfying. Our goal is to recreate that same experience on the web.
## Summary
As you can see, this doesn’t have to take hours out of your day, but it’s essential for the recreation process. Here’s a quick summary of the key points:
- The animation is fast and likely uses ease-out easing or a spring animation with no bounce. A spring animation is more likely since it’s common in iOS apps.
- The duration is short, probably not more than 300ms, depending on the spring settings.
- We need to crossfade the content, so we’ll use the popLayout mode on AnimatePresence.
- Bouncy buttons are crucial for the right feel of the interaction.
- Action buttons have a different animation, which I’m not sure I like.
The animation is fast and likely uses ease-out easing or a spring animation with no bounce. A spring animation is more likely since it’s common in iOS apps.
The duration is short, probably not more than 300ms, depending on the spring settings.
We need to crossfade the content, so we’ll use the popLayout mode on AnimatePresence.
Bouncy buttons are crucial for the right feel of the interaction.
Action buttons have a different animation, which I’m not sure I like.
With this in mind, we can start implementing the animation.
## Smoothness
It’s worth noting that mobile browsers don’t support 120Hz refresh rate like an app like Family does. So we won’t be able to achieve the exact same smoothness as the app, but we can get pretty close.
Desktop is even worse in my experience if you have 60hz or less. This component requires a certain smoothness for the opacity transition to feel right. That’s why I’d only use it a mobile component.
---
# The finishing touch
- Module: Family Drawer
- Lesson URL: /lessons/family-drawer/the-finishing-touch/
- Markdown URL: /md/lessons/family-drawer/the-finishing-touch.md
- Source mirror path: apps/anim/learn/family-drawer/the-finishing-touch.html
We have our height and crossfade animations working, but it doesn’t feel quite right yet. The drawer is too slow at the moment, it feels kinda robotic and the easing could be improved as well.
## Overview
We have our height and crossfade animations working, but it doesn’t feel quite right yet. The drawer is too slow at the moment, it feels kinda robotic and the easing could be improved as well.
When it comes to easing, I’ve tried a lot of different settings, and none of them felt great to me. It’s just trial and error like I told you before. In this case I landed on [0.26, 1, 0.5, 1] for the height animation, it’s a strong ease-out curve to make it snappy.
When it comes to the body, I wanted something slower to ensure that the content transition is visible. I couldn’t use something else than an ease-out curve though, because it would feel out of sync. I chose [0.26, 0.08, 0.25, 1], which is a lighter version of the height easing.
The duration was chosen after trying a bunch of stuff as well, I landed on 0.27s. It’s not a common duration, you usually go for 0.25s or 0.3s, but again, this just felt right to me after many tries.
This is a creative process and I want to emphasize the importance of trying stuff out. I always start with choosing the right easing and only then I change the duration as the duration largely depends on the easing you choose.
Here’s the same drawer, but with the updates I just described:
## Dynamic opacity transition
One thing that annoys me a bit is the amount of fade we see when we transition from a relatively short drawer to another short one. It’s not the end of the world, but I’d like to make it better.
Notice how much fade we see when the drawer transitions from default state to the "Remove Wallet" state.
We can’t just make it faster, as the transition from short to tall looks good. What we can do instead is make the duration of the opacity transition dynamic. Let’s try implementing it below!
```text
"use client";
import { useMemo, useState, useRef } from "react";
import { Drawer } from "vaul";
import useMeasure from "react-use-measure";
import { motion, AnimatePresence } from "motion/react";
import { DefaultView, Key, Phrase, RemoveWallet } from "./components";
import { CloseIcon } from "./icons";
export default function FamilyDrawer() {
const [isOpen, setIsOpen] = useState(false);
const [view, setView] = useState("default");
const [elementRef, bounds] = useMeasure();
const content = useMemo(() => {
switch (view) {
case "default":
return ;
case "remove":
return ;
case "phrase":
return ;
case "key":
return ;
}
}, [view]);
return (
<>
setIsOpen(false)}
/>
{content}
>
);
}
```
## Vaul’s duration
Vaul, the drawer library that we use comes with a default animation duration of 500ms. This is a bit too slow for our use case, let’s change it to 200ms. We make this change to make everything feel more cohesive. The drawer should feel like a single entity, from opening it to animating the height.
```css
[vaul-drawer] {
transition: transform 0.2s cubic-bezier(0.165, 0.84, 0.44, 1);
}
[vaul-overlay] {
transition: opacity 0.2s cubic-bezier(0.165, 0.84, 0.44, 1);
}
```
```css
[vaul-drawer] {
transition: transform 0.2s cubic-bezier(0.165, 0.84, 0.44, 1);
}
[vaul-overlay] {
transition: opacity 0.2s cubic-bezier(0.165, 0.84, 0.44, 1);
}
```
I chose a high duration of 500ms for Vaul, because it fits the iOS’ Sheet animation. This does look slow when you have a small drawer that is not touching the edge of the window. The 500ms animation looks way better if you have it styled like the iOS component, you can see an example here.
## Conclusion
And that’s basically it. While this component might look a bit complicated at first, it’s just a simple combination of height animation, AnimatePresence, and the right easing.
Here’s the full code. Feel free to play around with it, remix it, make it your own, and most importantly, have fun with it!
```text
"use client";
import { useMemo, useState, useRef } from "react";
import { Drawer } from "vaul";
import useMeasure from "react-use-measure";
import { motion, AnimatePresence } from "motion/react";
import { DefaultView, Key, Phrase, RemoveWallet } from "./components";
import { CloseIcon } from "./icons";
export default function FamilyDrawer() {
const [isOpen, setIsOpen] = useState(false);
const [view, setView] = useState("default");
const [elementRef, bounds] = useMeasure();
const previousHeightRef = useRef();
const content = useMemo(() => {
switch (view) {
case "default":
return ;
case "remove":
return ;
case "phrase":
return ;
case "key":
return ;
}
}, [view]);
const opacityDuration = useMemo(() => {
const currentHeight = bounds.height;
const previousHeight = previousHeightRef.current;
const MIN_DURATION = 0.15;
const MAX_DURATION = 0.27;
if (!previousHeightRef.current) {
previousHeightRef.current = currentHeight;
return MIN_DURATION;
}
const heightDifference = Math.abs(currentHeight - previousHeight);
previousHeightRef.current = currentHeight;
const duration = Math.min(
Math.max(heightDifference / 500, MIN_DURATION),
MAX_DURATION,
);
return duration;
}, [bounds.height]);
return (
<>
setIsOpen(false)}
/>
{content}
>
);
}
```
---
# Animating in public
- Module: Framer Motion
- Lesson URL: /lessons/framer-motion/animating-in-public/
- Markdown URL: /md/lessons/framer-motion/animating-in-public.md
- Source mirror path: apps/anim/learn/framer-motion/animating-in-public.html
I got my first job at an American start up through Twitter, because I shared my work there.
## Overview
I got my first job at an American start up through Twitter, because I shared my work there.
Guillermo Rauch reached out to me on Twitter as well, and that’s how I got my job at Vercel.
If I wasn’t posting my work on Twitter, you probably wouldn’t be taking this course right now, as you wouldn’t even know I exist.
What I’m trying to say here is that building in public is incredibly powerful. If you don’t have a job yet, it’ll increase your chances to get one and if you have a job already, it’ll strenghten your position on the market, or, if you want to start your own thing, you’ll already have an audience.
## What does this have to do with animations?
This is a homework lesson. I’d want you to craft an animation based on the things we’ve learned, and share it on Twitter or send it to me privately. You can tag me in the tweet and we can have a nice interaction there, I love seeing your work.
If you would want me to review the animation first, you can email me at e@emilkowal.ski and I’ll make sure to give you feedback.
## How to record animations
Here are a few tips:
- Focus on what you want to present, zoom in, make the motion easy to see.
- Record in light mode, stands out more on X, looks cleaner, and motion looks better.
- If on MacOS use Quicktime instead of Cleanshot or any other screen recorder for better smoothness.
---
# Feedback popover
- Module: Framer Motion
- Lesson URL: /lessons/framer-motion/feedback-popover/
- Markdown URL: /md/lessons/framer-motion/feedback-popover.md
- Source mirror path: apps/anim/learn/framer-motion/feedback-popover.html
A button that morphs into a popover.
## Overview
A button that morphs into a popover.
## Button to popover
Let’s animate the first part of the component where the feedback button becomes the feedback popover. You will need to be a bit creative here. One hint I’ll give you is that the gray feedback text inside the popover is not the actual placeholder of the textarea, it’s a separate html element.
Success criteria:
- The button should morph into the popover.
- The placeholder should disappear when the user starts typing.
```text
"use client";
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useState, useRef } from "react";
import { Spinner } from "./Spinner";
import { useOnClickOutside } from "usehooks-ts";
import "./styles.css";
export default function FeedbackComponentCSS() {
const [open, setOpen] = useState(false);
const [formState, setFormState] = useState(
"idle",
);
const [feedback, setFeedback] = useState("");
const ref = useRef(null);
useOnClickOutside(ref, () => setOpen(false));
function submit() {
setFormState("loading");
setTimeout(() => {
setFormState("success");
}, 1500);
setTimeout(() => {
setOpen(false);
}, 3300);
}
useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === "Escape") {
setOpen(false);
}
if (
(event.ctrlKey || event.metaKey) &&
event.key === "Enter" &&
open &&
formState === "idle"
) {
submit();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [open, formState]);
return (
{open ? (
Feedback
{formState === "success" ? (
Feedback received!
Thanks for helping me improve Sonner.
) : (
)}
) : null}
);
}
```
## Success state
Let’s now animate the success state, the form should disappear to the bottom, and the success ui should come from top, with a slight blur.
Success criteria:
- The form and the success ui should animate simultaneously.
- The success state should come from the top.
- Exit animation should morph the success state into the button.
```text
"use client";
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useState, useRef } from "react";
import { Spinner } from "./Spinner";
import { useOnClickOutside } from "usehooks-ts";
import "./styles.css";
export default function FeedbackComponentCSS() {
const [open, setOpen] = useState(false);
const [formState, setFormState] = useState(
"idle",
);
const [feedback, setFeedback] = useState("");
const ref = useRef(null);
useOnClickOutside(ref, () => setOpen(false));
function submit() {
setFormState("loading");
setTimeout(() => {
setFormState("success");
}, 1500);
setTimeout(() => {
setOpen(false);
}, 3300);
}
useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === "Escape") {
setOpen(false);
}
if (
(event.ctrlKey || event.metaKey) &&
event.key === "Enter" &&
open &&
formState === "idle"
) {
submit();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [open, formState]);
return (
);
}
```
## Accessibility
Even if we replace our placeholder with a span we should still provide a placeholder for the textarea, this is because the placeholder attribute is used by screen readers to provide context to the user. We can hide it visually by setting the opacity to 0.
## Key takeaways
layoutId is very powerful, and it’s even more powerful once you become a bit creative with it. In this case, we created an illusion. The placeholder is not an actual placeholder, but it looks like one.
Another good takeaway here is that the popLayout mode is often times the right mode for your animations. If you see your exit animation breaking, think about the mode prop.
---
# Hooks and animations
- Module: Framer Motion
- Lesson URL: /lessons/framer-motion/hooks-and-animations/
- Markdown URL: /md/lessons/framer-motion/hooks-and-animations.md
- Source mirror path: apps/anim/learn/framer-motion/hooks-and-animations.html
We’ve now animated things mainly using initial, animate and exit props. While these props are very powerful, and I personally use Framer Motion this way ~90% of the time, this library has more to offer.
## Overview
We’ve now animated things mainly using initial, animate and exit props. While these props are very powerful, and I personally use Framer Motion this way ~90% of the time, this library has more to offer.
Hooks like useSpring or useTransform can help us in cases where the animate prop is not enough. But before we dive into these hooks, we need to understand what motion values are.
## Motion values
Motion values are primitives from Framer Motion that update their value outside of React’s render cycle. That’s what allows us to animate at 60 frames per second as we are not triggering any re-renders each time the transform value changes for example.
In the recording below the transform property changes its value multiple times per second, but because it’s not caused by a change in the state, but rather a change in the motion value, the component won’t re-render.
To create a motion value we can use the useMotionValue hook. The string or number passed to the hook will act as its initial state. motion components can consume a motion value through the style property. The translateX value of the div below will be 100px for example.
```tsx
const x = useMotionValue(100);
```
```tsx
const x = useMotionValue(100);
```
We can update our motion values with the set method and read their value with the get method. Notice how in the demo below when we click on the button, the motion value updates, but the rendered value is not changed. That’s because these updates are happening outside of React’s render cycle like we discussed.
```text
"use client";
import { motion, useMotionValue } from "motion/react";
export default function MotionValueBasics() {
const x = useMotionValue(0);
return (
Motion value: {x.get()}
);
}
```
This is nice, but we are here for animations and these updates are instant. Let’s see how we can add some motion to our motion values.
## Animating motion values
The useSpring hook creates a motion value that animates to the new value with a spring animation. To make our yellow rectangle animate, we can simply replace useMotionValue with useSpring.
```tsx
const x = useSpring(0);
```
```tsx
const x = useSpring(0);
```
The type of spring can be changed by passing a second argument to the hook. Whenever I use useSpring multiple times in a component, I usually create a variable for the spring values.
```tsx
const SPRING = {
type: "spring",
damping: 10,
mass: 0.75,
stiffness: 100,
};
const x = useSpring(0, SPRING);
```
```tsx
const SPRING = {
type: "spring",
damping: 10,
mass: 0.75,
stiffness: 100,
};
const x = useSpring(0, SPRING);
```
This hook might not feel ground breaking, but let’s think about a component that renders a circle that follows your mouse position. How would you do it with the animate prop?
We would need to add an event listeners for pointermove and a state variable to store the mouse position which would trigger a re-render each time the mouse moves and assign that as the x and y values in the animate prop. This is okay-ish, but the performance is not great.
```tsx
// This component can re-render even 60 times per second
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
return (
)
```
How would the code for this look like if we were to use motion values? Would it be better or worse? Let’s find out! Your job will be to animate the circle to follow the mouse position using the hooks we’ve learned so far. The end result should look like this:
```text
"use client";
export default function MotionValueBasics() {
return (
);
}
```
The interaction above is relatively simple, but the same technique is used in this illustration below that I once made for Linear. It’s basically the same approach, we are following the mouse, the visual does most of the work here to make it look good.
We will build a similar component in the next lesson.
Using useSpring for this type of interactions really does make a difference. You can experience it yourself in the example below, try to toggle the animation on and off. No motion in this case feels lifeless while the spring animation makes it very satisfying.
## Transforming motion values
Let’s say we want our circle that follows the cursor to change its size based on the distance from the top of the wrapping div. How can we do that? We have to transform the y motion value. The useTransform hook can help us with that.
This hook creates a new motion value that transforms the output of one or more other motion values. Before I dive deeper into explaining how it works, let’s try to implement this effect in our component. We want our circle to scale up to 1.5 when it’s 300px (or more) from top of the screen, when it’s at the top it should be 1.
```text
"use client";
import { motion, useSpring } from "motion/react";
const SPRING = {
mass: 0.1,
damping: 16,
stiffness: 71,
};
export default function MotionValueBasics() {
const x = useSpring(0, SPRING);
const y = useSpring(0, SPRING);
const opacity = useSpring(0);
return (
);
}
```
When I first started using useTransform I remember it as confusing, 2 arrays right after each other, it was a bit hard to wrap my head around the syntax. But that’s why we start with an easy examples, and by no means beautiful examples.
Once you get comfortable with it, you will be able to use it to your advantage often. The Linear illustration I showed you earlier uses useTransform as well. It transforms the distance in pixels to percentages for the offsetDistance property so that the grey and yellow circles follow the mouse.
Another Linear illustration uses useTransform to transform the x value that is based on cursor position into width. The change in width is what makes the bars push each other so that it creates a momentum-like effect.
But a motion value doesn’t necesarily have to be something that you’d use in the style prop. You can also use it to animate numbers for example. In the intro animation below I set the motion value to 45 with a delay. It then interpolates it with a spring animation.
It’s the degree value on the left of the logo, it’s very subtle, but I did that on purpose as there was already a lot happening in this animation. The code for this would look roughly like this:
```tsx
const angleValue = useSpring(1, {
stiffness: 185,
damping: 25,
});
const angleDisplay = useTransform(angleValue,
value => `${Math.round(value)}°`
);
useEffect(() => {
const timer = setTimeout(() => {
angleValue.set(45);
}, 2600);
return () => clearTimeout(timer);
}, []);
```
```tsx
const angleValue = useSpring(1, {
stiffness: 185,
damping: 25,
});
const angleDisplay = useTransform(angleValue,
value => `${Math.round(value)}°`
);
useEffect(() => {
const timer = setTimeout(() => {
angleValue.set(45);
}, 2600);
return () => clearTimeout(timer);
}, []);
```
This introduces a different way of using useTransform. Instead of mapping a motion value, we are transforming the output of it. This is useful as it makes the useTransform hook subscribe to angleValue which is why when we render angleDisplay in our component it would show the up-to-date value unlike a plain useMotionValue hook.
## When to use which hook?
You might think that useSpring should be used for all animations because useMotionValue changes are instant. While I personally use useSpring more often, there are definitely cases in which useMotionValue is useful. Here’s one real-world use case.
Remember the app store gesture we’ve seen a while ago? Dragging to dismiss also changes the scale of the card. We transform the drag distance into a scale value using useTransform, but we want it to correspond directly to the drag distance. useSpring would feel disconnected from the gesture, because it would animate, so we use useMotionValue to directly update the scale.
useMotionValue becomes useful when working with gestures.
## Practice
These are the hooks that I use the most in my work. Let’s now practice them and discover a few additional ones by building some realistic examples.
---
# How do I code animations
- Module: Framer Motion
- Lesson URL: /lessons/framer-motion/how-do-i-code-animations/
- Markdown URL: /md/lessons/framer-motion/how-do-i-code-animations.md
- Source mirror path: apps/anim/learn/framer-motion/how-do-i-code-animations.html
In this lesson, we’ll cover some of the advanced features of Framer Motion that I often use.
## Overview
In this lesson, we’ll cover some of the advanced features of Framer Motion that I often use.
## Layout animations
The most powerful feature of this library is the layout prop. This is what enables our animations to feel native on the web. If we’d want to recreate the App Store’s card animation, we’d have to make the card we clicked on larger to cover the whole screen.
But how do we actually make that smooth transition on the web? We can’t animate it to position fixed. We’d probably need to measure the screen’s dimensions and spend a lot of time animating it.
That’s where layout animations come in. They allow us to animate any layout changes easily. Properties that were not possible to animate before, like flex-direction or justify-content, can now be animated smoothly by simply adding the layout prop.
Let’s imagine the yellow box below is the App Store card. It covers the whole screen when you click on it, but there is no transition. Try using Framer Motion to animate it. We are talking about layout animations, so you’ll probably need to use them. Here are the docs for reference.
```text
import { motion } from "motion/react";
import { useState } from "react";
export default function Example() {
const [open, setOpen] = useState(false);
return (
);
}
```
This is relatively simple to implement, but there is a lot of magic happening under the hood. Because of that, things might not always work as expected in some more complex animations that involve layout changes. You can experience some distortion, or other weird issues.
If you are interested in learning how layout animations work under the hood, you can read Inside Framer’s Magic Motion. It’s an article about reacreating Framer Motion’s layout animations from scratch.
## Shared layout animations
Thanks to shared layout animations, we can basically connect two elements and create a smooth transition between them. Below is an example of that. We are not actually moving the element to the center ourselves, the library does it for us.
## The Oddysey
Explore unknown galaxies.
## Angry Rabbits
They are coming for you.
## Ghost town
Scarry ghosts.
## Pirates in the jungle
Find the treasure.
## Lost in the mountains
Be careful.
We can also use it to make the highlight animation below. The highlight (div with gray color) is essentially rendered for the active element only.
- Saved Sites
- Collections
- 48 Following
- 32 Followers
This might be a bit confusing when you first see it so let’s break it down.
```tsx
{TABS.map((tab) => (
setActiveTab(tab)}
onMouseOver={() => setActiveTab(tab)}
onMouseLeave={() => setActiveTab(tab)}
>
{activeTab === tab ? (
) : null}
{tab}
))}
```
```tsx
{TABS.map((tab) => (
setActiveTab(tab)}
onMouseOver={() => setActiveTab(tab)}
onMouseLeave={() => setActiveTab(tab)}
>
{activeTab === tab ? (
) : null}
{tab}
))}
```
Here we have two rectangles. When we toggle the showSecond state value, one of them disappears, and the other one appears. Our goal is to have a smooth transition between them. Bonus points if you can get the button to transition smoothly as well, right now it just jumps.
```text
import { motion } from "motion/react";
import { useState } from "react";
export default function Example() {
const [showSecond, setShowSecond] = useState(false);
return (
{showSecond ? (
) : (
)}
);
}
```
This is obviously a pretty simple example, but we can already see how powerful this is. Shared layout animations also work when you navigate between pages. It opens so many doors to a more native web.
Let’s try and recreate the animated modals that we have seen before.
Here’s the same component, but without a smooth transition. Given what we just talked about, try to recreate the transition yourself.
```text
"use client";
import { useEffect, useState, useRef } from "react";
import { useOnClickOutside } from "usehooks-ts";
import { motion } from "motion/react";
export default function SharedLayout() {
const [activeGame, setActiveGame] = useState(null);
const ref = useRef(null);
useOnClickOutside(ref, () => setActiveGame(null));
useEffect(() => {
function onKeyDown(event) {
if (event.key === "Escape") {
setActiveGame(null);
}
}
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, []);
return (
<>
{activeGame ? (
<>
>
);
}
const GAMES = [
{
title: "The Oddysey",
description: "Explore unknown galaxies.",
longDescription:
"Throughout their journey, players will encounter diverse alien races, each with their own unique cultures and technologies. Engage in thrilling space combat, negotiate complex diplomatic relations, and make critical decisions that affect the balance of power in the galaxy.",
image:
"https://animations.dev/how-i-use-framer-motion/how-i-code-animations/space.png",
},
{
title: "Angry Rabbits",
description: "They are coming for you.",
longDescription:
"The rabbits are angry and they are coming for you. You have to defend yourself with your carrot gun. The game is not simple, you have to be fast and accurate to survive.",
image:
"https://animations.dev/how-i-use-framer-motion/how-i-code-animations/rabbit.png",
},
{
title: "Ghost town",
description: "Find the ghosts.",
longDescription:
"You are in a ghost town and you have to find the ghosts. But be careful, they are dangerous.",
image:
"https://animations.dev/how-i-use-framer-motion/how-i-code-animations/ghost.webp",
},
{
title: "Pirates in the jungle",
description: "Find the treasure.",
longDescription:
"You are a pirate and you have to find the treasure in the jungle. But be careful, there are traps and wild animals.",
image:
"https://animations.dev/how-i-use-framer-motion/how-i-code-animations/pirate.png",
},
{
title: "Lost in the mountains",
description: "Find your way home.",
longDescription:
"You are lost in the mountains and you have to find your way home. But be careful, there are dangerous animals and you can get lost.",
image:
"https://animations.dev/how-i-use-framer-motion/how-i-code-animations/boy.webp",
},
];
```
## Gestures
Framer Motion exposes a simple yet powerful set of UI gesture recognisers. That means that we can make a draggable component by simply adding the drag prop to a motion element for example.
As you can see in the demo above, the draggable element maintains momentum when dragging finishes, which helps it feel more natural. Usually, we want a simple drag functionality though. We can disable this effect by setting dragMomentum to false.
```tsx
"use client";
import { motion } from "motion/react";
import { useRef } from "react";
export function DragExample() {
const boundingBox = useRef(null);
return (
);
}
```
```tsx
"use client";
import { motion } from "motion/react";
import { useRef } from "react";
export function DragExample() {
const boundingBox = useRef(null);
return (
);
}
```
## App Store-like transition
Initially, this component was a separate lesson. But, I realized that it’s very similar to the shared layout exercise we did before. The design is just different. We will cover a Feedback popover instead, which isn’t as similar to the shared layout exercise and touches a few interesting topics.
Keep in mind that it’s not finished, there is a small text jump when you click on the card and in dark mode, there’s a slight white border present when animating. I might come back to it in the future and cover it more in-depth.
## Game of the day
A game about vikings
Are you ready? A game about vikings, where you can play as a viking and fight other vikings. You can also build your own viking village and explore the world.
The never ending adventureIn this game set in a fairy tale world, players embark on a quest through mystical lands filled with enchanting forests and towering mountains.
I did prepare the code for the App Store-like transition, so if you want to try it out, it’s right below. We won’t do an explanation video on this one, it’s just pure code. You can see it as an extra exercise. Try reading through the code and understanding it. If you have any questions, feel free to email me, I’m always happy to help.
Keep in mind that the drag interaction is not added here yet due to time constraints. I’ll add it in the near future though!
```text
"use client";
import { useState, useEffect, useRef } from "react";
import { AnimatePresence, motion } from "motion/react";
import "./styles.css";
import { useOnClickOutside } from "usehooks-ts";
function Card({ card, setActiveCard }) {
return (
setActiveCard(card)}
style={{ borderRadius: 20 }}
>
Game of the day
{card.title}
{card.description}
Get
Are you ready? {card.longDescription}
The never ending adventure
In this game set in a fairy tale world, players embark on a quest
through mystical lands filled with enchanting forests and towering
mountains.
The never ending adventure
In this game set in a fairy tale world, players embark on a quest
through mystical lands filled with enchanting forests and towering
mountains. Players can explore the world, build their own viking
);
}
const CARDS = [
{
title: "Vikings",
subtitle: "Clash of the Norse Warriors",
description: "A game about vikings",
longDescription:
"A game about vikings, where you can play as a viking and fight other vikings. You can also build your own viking village and explore the world.",
image:
"https://animations.dev/how-i-use-framer-motion/app-store-like-cards/game.webp",
logo: "https://animations.dev/how-i-use-framer-motion/app-store-like-cards/game-logo.webp",
},
];
```
## That’s it
These are the features of Framer Motion that I use the most. When you see one of my animations posted on Twitter, there’s a high chance it was done using the features we talked about in this lesson.
There are more features in Framer Motion, like reorder, useAnimate, etc. We will touch on some of them in the upcoming lessons. But in this module we talk about how I use Framer Motion, and I simply don’t use everything it offers. So we’ll focus on the features I think are the most useful.
---
# Interactive graph
- Module: Framer Motion
- Lesson URL: /lessons/framer-motion/interactive-graph/
- Markdown URL: /md/lessons/framer-motion/interactive-graph.md
- Source mirror path: apps/anim/learn/framer-motion/interactive-graph.html
In this lesson we’ll create an interactive graph to solidify our understanding of the hooks we just learned, but also to learn an additional hook. As always, it’s a real-world example, you can see a very similar component on Linear’s features page. This is how the end result will look like:
## Overview
In this lesson we’ll create an interactive graph to solidify our understanding of the hooks we just learned, but also to learn an additional hook. As always, it’s a real-world example, you can see a very similar component on Linear’s features page. This is how the end result will look like:
## Making it interactive
Before we add any motion, we first need to make the graph follow the cursor. The end result should look like this:
Our starting point will be a simple static svg graph. This exercise will cover what we just learned in the last lesson, but also a few techniques we have learned earlier in the course, and even a new Framer Motion hook. You can use the hints below if you get stuck. And remember, it’s okay to struggle, that’s how you learn. Good luck!
```text
"use client";
export default function Graph() {
return (
);
}
```
## Adding motion
The end result should look like the component below, pay attention to not only the motion, but also one detail that makes the graph feel nicer.
```text
"use client";
import { useMotionValue, useMotionTemplate, motion } from "motion/react";
export default function Graph() {
const clipPathValue = useMotionValue(0);
const clipPathTemplate = useMotionTemplate`inset(0px ${clipPathValue}% 0px 0px)`;
function onPointerMove(e) {
const rect = e.currentTarget.getBoundingClientRect();
const distanceFromRight = Math.max(rect.right - e.clientX, 0);
const percentageFromRight = Math.min(
(distanceFromRight / rect.width) * 100,
100,
);
clipPathValue.set(percentageFromRight);
}
return (
);
}
```
---
# Multi-step component
- Module: Framer Motion
- Lesson URL: /lessons/framer-motion/multi-step-component/
- Markdown URL: /md/lessons/framer-motion/multi-step-component.md
- Source mirror path: apps/anim/learn/framer-motion/multi-step-component.html
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 is step one
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.
## Step animation
Let’s try animating the steps first, with no height animation or direction awareness.
Success criteria:
- When the continue or back button is pressed, the exiting step should animate to the left and the entering step should animate from the right.
- The exit animation should consist of opacity and x transition.
```text
"use client";
import { useMemo, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import useMeasure from "react-use-measure";
import "./styles.css";
export default function MultiStepComponent() {
const [currentStep, setCurrentStep] = useState(0);
const content = useMemo(() => {
switch (currentStep) {
case 0:
return (
<>
This is step one
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.
>
);
case 1:
return (
<>
This is step two
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.
>
);
case 2:
return (
<>
This is step three
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.
>
);
}
}, [currentStep]);
return (
{content}
);
}
```
## Height animation
Animating the height will make our component look less jarring. Let’s try that next.
Success criteria:
- The height should animate smoothly when the content changes, it should not use magic numbers.
- The buttons should follow the height animation, they should not jump around.
```text
"use client";
import { useMemo, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import useMeasure from "react-use-measure";
import "./styles.css";
export default function MultiStepComponent() {
const [currentStep, setCurrentStep] = useState(0);
const content = useMemo(() => {
switch (currentStep) {
case 0:
return (
<>
This is step one
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.
>
);
case 1:
return (
<>
This is step two
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.
>
);
case 2:
return (
<>
This is step three
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.
>
);
}
}, [currentStep]);
return (
{content}
);
}
```
## Direction awareness
Currently, our elements enter from the right and disappear to the left, regardless of whether we are moving forward or backward. We want to flip our x-values depending on the direction we are moving towards. Good luck!
Success criteria:
- When the continue button is pressed, the entering step should animate from the right and the exiting step should animate to the left.
- When the back button is pressed, the entering step should animate from the left and the exiting step should animate to the right.
```text
"use client";
import { useMemo, useState } from "react";
import { AnimatePresence, motion, MotionConfig } from "motion/react";
import useMeasure from "react-use-measure";
import "./styles.css";
export default function MultiStepComponent() {
const [currentStep, setCurrentStep] = useState(0);
const [ref, bounds] = useMeasure();
const content = useMemo(() => {
switch (currentStep) {
case 0:
return (
<>
This is step one
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.
>
);
case 1:
return (
<>
This is step two
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.
>
);
case 2:
return (
<>
This is step three
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.
>
);
}
}, [currentStep]);
return (
{content}
);
}
```
## Accessibility
There is quite a lot movement happening on the screen. To disable the animation for people that have reduced motion enabled, we can use the useReducedMotion hook from Framer Motion. We haven’t done it in this exercise, as I wanted to keep the code relatively simple, but we we’ll talk about it in the accessibility lesson.
## Key takeaways
When an element is animating out using AnimatePresence its state is stale. To work around this, you can use the custom prop on our motion.div as well as on AnimatePresence. This will ensure that all leaving components are using the latest data.
## Rapid switching
When you switch quickly between states in animate presence, you might eventually see both elements being visible in the DOM. That’s a bug in Framer Motion that will hopefully be fixed. If you are experiencing it and you can’t work around it, I suggest installing version 11.0.10 of Framer Motion where this bug is not present.
## Inspiration
This exercise is inspired by buildui.com where Sam Selikoff talks about a similar problem. I’m a huge admirer of his work and I highly recommend checking out his content.
---
# The Basics
- Module: Framer Motion
- Lesson URL: /lessons/framer-motion/the-basics/
- Markdown URL: /md/lessons/framer-motion/the-basics.md
- Source mirror path: apps/anim/learn/framer-motion/the-basics.html
While Framer Motion is a very powerful animation library and is relatively easy to start with, it still differs from CSS animations. We’ll go over the basics first and get comfortable with the library.
## Overview
While Framer Motion is a very powerful animation library and is relatively easy to start with, it still differs from CSS animations. We’ll go over the basics first and get comfortable with the library.
## Anatomy of an animation
To animate with Framer Motion we need to use the motion element. It’s a wrapper around the native HTML elements that allows us to animate them using Framer Motion’s API.
## Framer Motion imports
Framer Motion is now Motion for React. We still use motion imports in this course, but this doesn’t change much, the API of the library is the same. The only difference is the import path. You can read more about it here.
```text
import { motion } from "motion/react";
import { useState } from "react";
export default function Example() {
return (
)
}
```
When working with animations you have a start and an end state. In Framer Motion, you can define these states using the initial and animate props. The initial prop defines the starting state of the animation, while the animate prop defines the end state.
Let’s say we want to create an enter animation for our yellow rectangle. We can define our initial state as follows:
```jsx
```
```jsx
```
This will make our element invisible when it renders the first time (at mount time). To define our end state, we simply add an animate prop, and define the properties we want to end up with:
```jsx
```
```jsx
```
To view our effect, reload the sandbox by pressing on the refresh button in the top right corner.
```text
import { motion } from "motion/react";
import { useState } from "react";
export default function Example() {
return (
)
}
```
When we inspect a Framer Motion animation we can see that the value we provided isn’t immediately applied. It’s interpolated, which means that the value is gradually changed. That’s because the animation is done in Javascript and not CSS.
This is not the element above, but it’s a similar example.
The animation happens outside of React’s render cycle, so every time the animation updates, it doesn’t trigger a re-render, which is great for performance.
## Transition prop
By default, Framer Motion will create an appropriate animation for a snappy transition based on the types of value being animated. For instance, physical properties like x or scale will be animated via a spring simulation. Whereas values like opacity or color will be animated with a tween (easing-based).
We can also define our own transition using the transition prop. It takes in an object with properties like duration, type, delay, and more.
You can see both of the animation types below.
```jsx
// Spring animation
// Easing animation
```
```jsx
// Spring animation
// Easing animation
```
## Exit animations
Exit animations in React are hard. AnimatePresence in Framer Motion allows components to animate out when they’re removed from the React tree.
It has good DX as well. All you need to do is wrap an element you want to animate out with AnimatePresence, and add the exit prop, which works the same way as initial and animate, except it defines the end state of our component when it’s removed.
```jsx
import { motion, AnimatePresence } from "motion/react"
export const MyComponent = ({ isVisible }) => (
{isVisible ? (
) : null}
)
```
```jsx
import { motion, AnimatePresence } from "motion/react"
export const MyComponent = ({ isVisible }) => (
{isVisible ? (
) : null}
)
```
It also has different modes. I often use the wait mode to animate between two elements. The copy button we’ve seen earlier in this lesson is using this mode. When you click the button, the copy icon animates out, and only after that, the checkmark animates in.
```jsx
const variants = {
hidden: { opacity: 0, scale: 0.5 },
visible: { opacity: 1, scale: 1 },
};
// ...
```
```jsx
const variants = {
hidden: { opacity: 0, scale: 0.5 },
visible: { opacity: 1, scale: 1 },
};
// ...
```
With this type of animations, it’s important to include initial={false} on AnimatePresence. This tells Framer Motion not to animate on the initial render.
## Tip
If an animation that involves AnimatePresence is not working as expected, make sure that you have a key prop on the element you’re animating. Otherwise, the component won’t be unmounted and the exit animation won’t be triggered.
AnimatePresence is also the key for this button animation, which is used on the login page of this course.
Let’s try and recreate it.
```text
"use client";
import { useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import { Spinner } from "./Spinner";
import "./styles.css";
const buttonCopy = {
idle: "Send me a login link",
loading: ,
success: "Login link sent!",
};
export default function SmoothButton() {
const [buttonState, setButtonState] = useState("idle");
return (
);
}
```
## Variants
We can also make a reusable component out of this, so that it doesn’t have to be used only for this button. Here, I also used variants. Variants are predefined sets of targets which can be then used in the initial, animate, and exit props.
They can be useful if you find yourself repeating the same animation multiple times. In this case, I knew that enter and exit animation would be the same, so I used it, but there are definitely better use cases for it.
```tsx
"use client";
import { AnimatePresence, motion } from "motion/react";
const variants = {
initial: { opacity: 0, y: -25 },
visible: { opacity: 1, y: 0 },
exit: { opacity: 0, y: 25 },
};
export function AnimatedState({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
```tsx
"use client";
import { AnimatePresence, motion } from "motion/react";
const variants = {
initial: { opacity: 0, y: -25 },
visible: { opacity: 1, y: 0 },
exit: { opacity: 0, y: 25 },
};
export function AnimatedState({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
## Animating height
Framer Motion allows us to animate height from a fixed value to auto. The problem we are having here is that we want to animate from auto to auto, which Framer Motion doesn’t support. But often times we need to animate dynamic heights, like the Family drawer below. We will build the full version of the drawer in the walkthroughs later on by the way!
How are we supposed to handle dynamic heights? Below is a simple example where clicking the button changes the content, which results in height change. Try to animate the height automatically when the content changes.
This one is not easy, it might be tricky, and perhaps frustrating. However, struggling and failing is much more productive than reading through the solution. When I started to learn how to code I didn’t want to struggle and I regret it. There is a small hint in the code below though!
```text
import { motion } from "motion/react";
import { useState, useRef, useEffect } from "react";
import useMeasure from 'react-use-measure'
export default function Example() {
const [showExtraContent, setShowExtraContent] = useState(false);
return (
Fake Family Drawer
This is a fake family drawer. Animating height is tricky, but
satisfying when it works.
{showExtraContent ? (
This extra content will change the height of the drawer. Some even more content to make the drawer taller and taller and taller...
) : null}
);
}
```
## When should you use Framer Motion?
Before we dive into more advanced concepts of Framer Motion, let’s talk about when you should use it.
I tend to avoid using this library if I can achieve the same effect with CSS in a reasonable amount of time. Basically, I animate all enter and exit animations of UI elements like modals, dropdowns etc. through Radix, as it allows me to animate the exit with CSS.
```css
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.dialog[data-state="open"] {
animation: fadeIn 200ms ease-out;
}
.DialogContent[data-state="closed"] {
animation: fadeOut 150ms ease-out;
}
```
```css
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.dialog[data-state="open"] {
animation: fadeIn 200ms ease-out;
}
.DialogContent[data-state="closed"] {
animation: fadeOut 150ms ease-out;
}
```
Radix listens for animationstart event, and if an animation started when the open state changed to false they suspend the unmount while the animation plays out.
Whether to use Framer Motion or not also depends on how sensitive your app is to bundle size. At Vercel, we avoided using Framer Motion in Next.js’ docs, because we wanted to keep the bundle size as small as possible.
The copy animation below was initially using Framer Motion, as it was easier to build and maintain. But, we ended up switching to CSS animations to not use this library and reduce the bundle size.
But at Clerk, we are already using Framer Motion in our dashboard, so I reach for it more easily there to save time and make the code easier to read and maintain.
An animation in Clerk's dashboard that uses Framer Motion.
Framer Motion also has a guide on how to reduce bundle size which is worth checking out if you are concerned about it.
---
# Trash interaction
- Module: Framer Motion
- Lesson URL: /lessons/framer-motion/trash-interaction/
- Markdown URL: /md/lessons/framer-motion/trash-interaction.md
- Source mirror path: apps/anim/learn/framer-motion/trash-interaction.html
To get the codebase running locally, you first need to download it here. We switched from a GitHub repo, because it would be quite problematic to keep it private.
## Installation
To get the codebase running locally, you first need to download it here. We switched from a GitHub repo, because it would be quite problematic to keep it private.
After you have it on your device, you can run pnpm install and pnpm dev after.
The final code can be downloaded here.
## Toolbar animation
The toolbar appears once we select at least one image, and it disappears when we deselect all images. Because we need to hide it, we should wrap our toolbar in AnimatePresence and add an appropriate animation to it.
```jsx
{imagesToRemove.length > 0 && !readyToRemove ? (
) : null}
;
```
## Transition into the trash
We’ll need to use the layoutId prop here. That way we tell Framer Motion that the images in the grid should become the images in the trash. Let’s add the prop to both the grid and the trash images.
```jsx
;
```
```jsx
;
```
## Trash enter and exit animation
We’ll wrap our trash with AnimatePresence and add a small animation to it.
```jsx
;
```
```jsx
;
```
## Instant removal fix
If we don’t select all images, the unselected ones disappear instantly. To fix that, we can wrap our images grid in AnimatePresence and add an exit animation to it (the addition of an exit animation is not included in the video).
```jsx
```
```jsx
```
## Images are going through the trash
To fix the issue with images going through the trash, we can ensure that our component becomes visible after our images are almost done animating. We need to time it essentially.
```jsx
;
```
```jsx
;
```
## Motion Config
I’m not the biggest fan of the transition right now, but I don’t want to adjust it for each item separately. This is a perfect use case for MotionConfig.
```jsx
// ...
```
```jsx
// ...
```
## Dropping into the bin
We now need to create this nice drop animation. However, we can’t influence the way shared layout animations are done. What we can do is we can animate the parent div so that the images will follow it.
```jsx
// ...
;
```
```jsx
// ...
;
```
## Key takeaways
- We can animate the parent of our shared layout animation to influence how the children are animated.
- MotionConfig is a great way to set a default transition for all animations in a component.
---
# Why Framer Motion
- Module: Framer Motion
- Lesson URL: /lessons/framer-motion/why-framer-motion/
- Markdown URL: /md/lessons/framer-motion/why-framer-motion.md
- Source mirror path: apps/anim/learn/framer-motion/why-framer-motion.html
Framer Motion allows us to create very impressive, native-like animations with not a lot of code. Because of that, many things happen magically, and once you run into an issue it can be hard to debug.
## Overview
Framer Motion allows us to create very impressive, native-like animations with not a lot of code. Because of that, many things happen magically, and once you run into an issue it can be hard to debug.
## The Oddysey
Explore unknown galaxies.
## Angry Rabbits
They are coming for you.
## Ghost town
Scarry ghosts.
## Pirates in the jungle
Find the treasure.
## Lost in the mountains
Be careful.
The documentation often times follows a happy path. It covers simple animations, which are great for beginners, but don’t give you a lot of insight on how to craft more complex ones.
That’s why, in this part, we’ll build a lot. We’ll cover the basics, but we’ll quickly transition into building more complex animations like the Feedback popover below. We’ll run into issues and solve them, so that you know what to do when you run into similar problems yourself.
Before we start coding, let’s see why Framer Motion is so powerful and what makes it stand out from other libraries.
## The power of Framer Motion
Everything that is possible with Framer Motion is possible with vanilla CSS and JS as well. It just usually takes a lot more time.
How would you code the animation below without this library? You’d need to calculate the highlight’s new position based on the tab’s dimensions and its distance from the left side. That’s only the highlight part. Now, we’d need to find a way to smoothly change the direction (button in the bottom right corner) as well.
- Saved Sites
- Collections
- 48 Following
- 32 Followers
This whole component, including the styles and inline SVGs, is only 76 lines of code. That alone would convince me to start using Framer Motion for more complex animations. You can view my implementation of this animation here.
Remember the example below when we discussed spring animations? It consists of 33 lines of code, is interruptible and maintains momentum. Now, try building it again without Framer Motion or a similar library. It’d be a lot of work.
Also, in React, it’s usually difficult to animate components once they’ve been removed from the DOM, because, well... they’re not there anymore. By wrapping your components with AnimatePresence provided by Framer Motion, you can animate them out as well.
```jsx
import { motion, AnimatePresence } from "motion/react";
export function Component({ isVisible }: { isVisible: boolean }) {
return (
{isVisible ? (
) : null}
);
}
```
```jsx
import { motion, AnimatePresence } from "motion/react";
export function Component({ isVisible }: { isVisible: boolean }) {
return (
{isVisible ? (
) : null}
);
}
```
Additionally, thanks to layout animations, which we will cover in the next lesson, we are able to easily create interactions like the one below.
You can also create drag-and-drop interactions, draw svgs, and more.
Framer Motion is not the only animation library out there though. GSAP and react-spring are two popular alternatives. Let’s see how they compare.
## React Spring
A spring-based library, powerful, and highly configurable. It has a steep learning curve, but at the same time it gives you a lot of control.
Below you can see how you can create a simple fade-in animation with React Spring.
```jsx
import { useSpring, animated } from "@react-spring/web";
function MyComponent() {
const [props, api] = useSpring(
() => ({
from: { opacity: 0 },
to: { opacity: 1 },
}),
[],
);
return Hello World;
}
```
```jsx
import { useSpring, animated } from "@react-spring/web";
function MyComponent() {
const [props, api] = useSpring(
() => ({
from: { opacity: 0 },
to: { opacity: 1 },
}),
[],
);
return Hello World;
}
```
It’s made by Poimandres, and because of that, this library works very well with other Poimandres libraries like use-gesture for example.
The code of this example is available here.
Pros:
- Spring-based animations.
- Smaller package size than Framer Motion.
- Highly-configurable.
Cons:
- Steep learning curve.
- It takes more time and more code to write the same animations as in Framer Motion.
- Documentation is hard to parse at times.
## GSAP
The first animation library I’ve ever used. We’ve talked about websites on awwwwards in the lesson about taste, GSAP powers a lot of these. It’s framework-agnostic and doesn’t support spring animations. I see framework-agnostic as a con, because framework-specific libraries usually provide a better developer experience. Though GSAP has a React specific hook which you can see below.
```jsx
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
gsap.registerPlugin(useGSAP);
const container = useRef();
useGSAP(
() => {
gsap.to(".box", { opacity: 1 });
},
{ scope: container },
);
```
```jsx
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
gsap.registerPlugin(useGSAP);
const container = useRef();
useGSAP(
() => {
gsap.to(".box", { opacity: 1 });
},
{ scope: container },
);
```
It’s open-source, but you can’t use it in commercial projects on the free plan. Additionally, it has a rich plugin system, but some plugins are only available on the paid plan. You can see the pricing here.
## Update to GSAP’s pricing
Since this lesson was published, GSAP has been acquired by Webflow and all of its features are now free.
This is made with GSAP.
Pros:
- A very useful timeline feature.
- Easier to learn than the alternatives (in my opinion).
- Framework-agnostic can be seen as a pro to some.
- Large community and good documentation.
Cons:
- Some plugins require a paid plan.
- No spring animations.
- Not tailored to React.
- Paid plan for commercial projects.
## Framer Motion
You can write declarative and imperative animations with it. Declarative ones can be defined as simple descriptions. This API avoids the need for you to interact with the DOM via selectors, refs or other kinds of error-prone wiring. There’s less room for error and it’s easier to scale.
```jsx
```
```jsx
```
Imperative animations are more low-level, more powerful, but harder to write and maintain. You can read more about the differences here. I personally almost never use imperative animations myself.
```jsx
function ImperativeComponent() {
const [scope, animate] = useAnimate();
useEffect(() => {
// This "li" selector will only select children
// of the element that receives `scope`.
animate("li", { opacity: 1 });
});
return
{children}
;
}
```
```jsx
function ImperativeComponent() {
const [scope, animate] = useAnimate();
useEffect(() => {
// This "li" selector will only select children
// of the element that receives `scope`.
animate("li", { opacity: 1 });
});
return
{children}
;
}
```
Thanks to layout animations Framer Motion can animate properties that are not possible to animate with CSS, like flex-direction. The direction change in the example from the beginning of this lesson is done by simply changing the flex-direction from row to column.
- Saved Sites
- Collections
- 48 Following
- 32 Followers
By using shared layout animations we can connect two components and create a transition between them when the first one is removed. That’s how the trash interaction is done. We aren’t moving the images ourselves. The images in the trash are completely different elements than the selectable ones. By using the same layoutId we tell Framer Motion to animate them to the place where the images in the trash will be rendered.
My favorite example that showcases the power of Framer Motion is the one below. It feels incredibly native to me.
Because these animations are relatively easy to build, there’s a lot of "magic" that is happening behind the scenes. I often run into a situation where something is not working, and I can’t understand why, because I don’t know how Framer Motion works under the hood.
Docs often follow the happy path, which is great for beginners, but when you run into an issue that is not covered in the docs, it can be hard to find a solution.
Pros:
- Spring animations.
- Layout animations are an absolute game-changer.
- Complex animations with not a lot of code.
- The API ties in well with React.
Cons:
- Bundle size.
- Sometimes, it’s hard to understand why an animation is not working as expected.
## Framer Motion is now Motion
Framer Motion has been renamed to Motion for React. Nothing except the import path has changed. To import motion from Motion for React you need to do the following:
```jsx
import { motion } from "motion/react";
```
```jsx
import { motion } from "motion/react";
```
Previously, you would’ve imported it like this:
```jsx
import { motion } from "framer-motion";
```
```jsx
import { motion } from "framer-motion";
```
I will keep referring to it as Framer Motion in this course to avoid confusion as mentioned in the CSS Module, but the imports in all exercises have been updated.
With that being said, Motion (not Motion for React) is a good alternative to Framer Motion when you’re not using React. You can read more about it here. If you are working with React, I would still recommend Framer Motion as it’s more tailored to the framework.
## Other alternatives
We haven’t covered every library out there, only the most popular ones. There are many other libraries like Anime.js or Popmotion, but I haven’t tried any of those and basically stick to Framer Motion.
There’s also the Web Animation API, which is a native browser API for animations. It’s not as powerful as the libraries we’ve discussed, but it’s definitely something you can check out.
Let’s start building some animations now!
---
# Accessibility
- Module: Good vs Great Animations
- Lesson URL: /lessons/good-vs-great-animations/accessibility/
- Markdown URL: /md/lessons/good-vs-great-animations/accessibility.md
- Source mirror path: apps/anim/learn/good-vs-great-animations/accessibility.html
The interactive examples and code snippets in this article are crucial to understanding the concepts like in the last lesson, so this one is text-based as well, but there is an exercise with a video for the solution.
## Overview
The interactive examples and code snippets in this article are crucial to understanding the concepts like in the last lesson, so this one is text-based as well, but there is an exercise with a video for the solution.
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.
Most devices today allow users to convey their preference for animations.
You can read this preference in browsers using the prefers-reduced-motion CSS media query.
- prefers-reduced-motion: no-preference means the user has not set a preference. No changes to your animation needed.
- prefers-reduced-motion: reduce means the user has set a preference. Your animation should be altered.
When that preference is present, we should consider removing, reducing or replacing animations.
## Workflow
Here’s a workflow for you to use when building animations:
- Build the animation.
- Make changes to account for prefers-reduced-motion. You can test prefers-reduced-motion being on with the browser’s DevTools.
- Ship the animation with 2 variants: 1 for prefers-reduced-motion: no-preference and 1 for prefers-reduced-motion: reduce.
## How should animations be changed?
To prevent people from feeling sick or getting distracted, our animations need to be changed based on these general guidelines:
- Disable autoplaying animations.
- Only animate properties like opacity, color, background-color. Avoid animating properties like transform that adds motion or changes layout, ensure that no elements move.
reduced-motion does not mean no animations. Remember, animations are used to make our UIs easier to understand. Disabling animations all together would reduce the understandability of our UIs. Animations should help convey meaningful information to the user.
Here’s a modal that opens with a scale animation. Everytime you click the button, a fake prefers-reduced-motion media query is toggled and the animation is adjusted.
## How to implement it in code?
We can use a media query in CSS to adjust the animation based on the user’s preference:
```css
.element {
animation: bounce 0.2s;
}
@media (prefers-reduced-motion: reduce) {
.element {
animation: fade 0.2s;
}
}
```
```css
.element {
animation: bounce 0.2s;
}
@media (prefers-reduced-motion: reduce) {
.element {
animation: fade 0.2s;
}
}
```
Tailwind provides a utility class for this media query:
```html
```
```html
```
Framer Motion gives us a useReducedMotion hook that returns true if the user prefers reduced motion. We can use this hook to adjust our animations.
You can read more about this implementation here.
```jsx
import { useReducedMotion, motion } from "motion/react"
export function Sidebar({ isOpen }) {
const shouldReduceMotion = useReducedMotion();
const closedX = shouldReduceMotion ? 0 : "-100%";
return (
)
}
```
```jsx
import { useReducedMotion, motion } from "motion/react"
export function Sidebar({ isOpen }) {
const shouldReduceMotion = useReducedMotion();
const closedX = shouldReduceMotion ? 0 : "-100%";
return (
)
}
```
You can also use this hook without Framer Motion, this is how a dependency-free version looks like:
```jsx
import { useState, useRef, useEffect } from "react";
export function useReducedMotion(): boolean {
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
const { current: mediaQuery } = useRef(
window ? window.matchMedia("(prefers-reduced-motion: reduce)") : null
);
useEffect(() => {
const listener = () => {
setPrefersReducedMotion(!!mediaQuery.matches);
};
mediaQuery.addEventListener("change", listener);
return () => {
mediaQuery.removeEventListener("change", listener);
};
}, [mediaQuery]);
return prefersReducedMotion;
}
```
```jsx
import { useState, useRef, useEffect } from "react";
export function useReducedMotion(): boolean {
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
const { current: mediaQuery } = useRef(
window ? window.matchMedia("(prefers-reduced-motion: reduce)") : null
);
useEffect(() => {
const listener = () => {
setPrefersReducedMotion(!!mediaQuery.matches);
};
mediaQuery.addEventListener("change", listener);
return () => {
mediaQuery.removeEventListener("change", listener);
};
}, [mediaQuery]);
return prefersReducedMotion;
}
```
We’ve talked about MotionConfig before, it’s also useful for respecting the user’s preference for reduced motion. When the reducedMotion prop is set to user, Framer Motion will respect the user’s preference and animate opacity and backgroundColor only. The default here is never so you need to set it yourself.
```jsx
import { MotionConfig } from "motion/react";
// ...
{children}
```
```jsx
import { MotionConfig } from "motion/react";
// ...
{children}
```
This could be a wrapper around your whole application, that way you don’t have to remember to adjust the animation for reduced motion every time.
## Mental model for Visuals
Anything that looks like an image or video. These are often used to add a visual metaphor for concepts. Like this visual we’ve seen before:
Here we could jump to each frame, instead of animating between them. We can’t remove the animation altogether, as it’s essential to understanding the concept.
## Snippets
Here are a few snippets that can help you make your UI more accessible.
## Disable smooth scrolling
When no preference, enables smooth scrolling on the document, when reduced-motion disables smooth scrolling.
```css
@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}
```
```css
@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}
```
## Autoplaying images
When no preference, shows a looping animated image, when reduced-motion shows a static fallback.
```html
```
```html
```
## Autoplaying videos
When no preference, autoplays the video, when reduced-motion shows the video paused and with controls to manually start the video. This is a vanilla version, but it can be easily adapted to any framework.
```html
{
// Remove the default video controls to allow for custom control implementation
video.removeAttribute('controls');
// Make the custom play button visible
btn.hidden = false;
// Enable autoplay if the user has not expressed a preference for reduced motion
if (noMotionPreference.matches) {
video.setAttribute('autoplay', true);
btn.innerText = 'Pause'; // Set the button text to "Pause" as the video will start playing
}
};
// Set up an event listener for the play button to control video playback
btn.addEventListener('click', () => {
// Check if the video is currently paused
if (video.paused) {
video.play(); // Play the video
btn.innerText = 'Pause'; // Update the button text to "Pause"
} else {
video.pause(); // Pause the video
btn.innerText = 'Play'; // Update the button text to "Play"
}
});
// Call the function to initialize the video setup
initVideo();
```
```js
// Query the DOM for the button and video elements using their data attributes
const btn = document.querySelector('button');
const video = document.querySelector('video');
// Check if the user has a preference for reduced motion
const noMotionPreference = window.matchMedia('(prefers-reduced-motion: no-preference)');
/**
* Initializes the video player by configuring UI elements and autoplay settings.
*/
const initVideo = () => {
// Remove the default video controls to allow for custom control implementation
video.removeAttribute('controls');
// Make the custom play button visible
btn.hidden = false;
// Enable autoplay if the user has not expressed a preference for reduced motion
if (noMotionPreference.matches) {
video.setAttribute('autoplay', true);
btn.innerText = 'Pause'; // Set the button text to "Pause" as the video will start playing
}
};
// Set up an event listener for the play button to control video playback
btn.addEventListener('click', () => {
// Check if the video is currently paused
if (video.paused) {
video.play(); // Play the video
btn.innerText = 'Pause'; // Update the button text to "Pause"
} else {
video.pause(); // Pause the video
btn.innerText = 'Play'; // Update the button text to "Play"
}
});
// Call the function to initialize the video setup
initVideo();
```
## Providing a “hero” for looping animations
```css
.animation {
animation: shake 0.2s infinite;
}
@media (prefers-reduced-motion: reduce) {
.animation {
animation-play-state: paused;
/* Pauses the animation on the frame at 0.4s Try different values and see which frame looks the best. */
animation-delay: -0.4s;
}
}
```
```css
.animation {
animation: shake 0.2s infinite;
}
@media (prefers-reduced-motion: reduce) {
.animation {
animation-play-state: paused;
/* Pauses the animation on the frame at 0.4s Try different values and see which frame looks the best. */
animation-delay: -0.4s;
}
}
```
Vercel does that on their rendering page. When the user prefers reduced motion, the animation is paused on a frame from the animation.
## Exercise
Our multi-step component that we built earlier doesn’t respect the user’s preference for reduced motion. Let’s fix that. When the user prefers reduced motion, we should only animate the opacity of the steps.
```text
import { useMemo, useState } from "react";
import { AnimatePresence, motion, MotionConfig } from "motion/react";
import useMeasure from "react-use-measure";
import "./styles.css";
export default function MultiStepComponent() {
const [currentStep, setCurrentStep] = useState(0);
const [direction, setDirection] = useState();
const [ref, bounds] = useMeasure();
const content = useMemo(() => {
switch (currentStep) {
case 0:
return (
<>
This is step one
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.
>
);
case 1:
return (
<>
This is step two
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.
>
);
case 2:
return (
<>
This is step three
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.
>
);
}
}, [currentStep]);
return (
{content}
);
}
const variants = {
initial: (direction) => {
return { x: `${110 * direction}%`, opacity: 0 };
},
active: { x: "0%", opacity: 1 },
exit: (direction) => {
return { x: `${-110 * direction}%`, opacity: 0 };
},
};
```
---
# Animations of the future
- Module: Good vs Great Animations
- Lesson URL: /lessons/good-vs-great-animations/animations-of-the-future/
- Markdown URL: /md/lessons/good-vs-great-animations/animations-of-the-future.md
- Source mirror path: apps/anim/learn/good-vs-great-animations/animations-of-the-future.html
Static transitions can disrupt the user’s sense of flow and orientation. You should envision your interface as a constantly evolving space, where any element can transform into another.
## Overview
Static transitions can disrupt the user’s sense of flow and orientation. You should envision your interface as a constantly evolving space, where any element can transform into another.
Family's iOS App is a great example of a fluid interface. This video is made by Benji,
We did this in the course already with shared layout animations. Rather than rendering a separate component for the form, we morph our button into its new state.
Another example of it, is the trash interaction we made. The items don’t suddenly appear in the trash, they are "thrown" into it.
## What about text?
Applying this principle to text adds another layer of fluidity.
This is not only appealing, but it also helps the user notice the change. In a static interface, this change would happen instantly. By morphing the text we highlight the transition in a subtle, yet effective way.
When I shared this transition on Twitter, some people said that this is distracting, but that’s exactly the point! By morphing the text we want to emphasize the change, we want to let the user know that the button he is about to press has different consequences.
## Fluidity and perceived performance
Seamless movement in our app can improve user’s perception of speed. Below you can see Family’s graph transition on the left, and CashApp’s static transition on the right. The one on the left seems faster, even though it takes roughly the same time to load.
This video is made by Benji, the idea is heavily inspired by Benji as well
## Fluid interfaces are hard, especially on the web
A fluid interface on the web, is a seamless interface. Navigating through such interface is effortless for the user. iOS has the tools that enable you to easily achieve this (Swift UI), and, there’s much less screen estate, there are more opportunities for meaningful transitions.
I built the component below to demonstrate where a fluid web interface makes sense. The illustration transforms into an input. Additionally, after the input has a value, we morph the paste button into an "X" button. All of this feels like a natural progression, it gives you more joy than a static transition.
There are a lot of flaws in this animation, because I time-boxed myself to 60 minutes to build this component. I wanted to show that perfecting fluid animations is time-consuming, and that’s why you don’t see them a lot.
## Add Users to your Group
Start by adding users to your group or skip this step and do it later.
In order to achieve a fluid interface, you need to think about the animations before you design the UI. The illustration in the example above is positioned and styled in a way that makes the transition seamless.
## Should I always try to build fluid interfaces?
I can’t think of any other app that nails fluidity as well as Family does. You don’t see them a lot, because it’s time-consuming and requires a lot of thought. If you have the resources and the time, then you should do it. If your product misses features, has bugs, or lacks other important aspects, then you should focus on those first, otherwise, the fluidity will be overshadowed by those issues.
On top of that, the level of fluidity we talked about here is very hard to achieve on the web. I truly believe that more and more companies will start to invest into this, but for now, the technology is not quite there yet.
This chapter is called "Good vs Great animations" though, so I wanted to touch on this subject. I think it’s important to know what great looks like, even if it’s not really realistic at the moment I believe that this is the future of great web animations.
## Disclaimer
This lesson is heavily inspired by the way Benji thinks about motion design. The work of his team can be seen in Family.
---
# Performance
- Module: Good vs Great Animations
- Lesson URL: /lessons/good-vs-great-animations/performance/
- Markdown URL: /md/lessons/good-vs-great-animations/performance.md
- Source mirror path: apps/anim/learn/good-vs-great-animations/performance.html
The interactive examples in this article are crucial to understanding the concepts, which is why I decided to make this lesson text-only.
## Overview
The interactive examples in this article are crucial to understanding the concepts, which is why I decided to make this lesson text-only.
Making animations that are performant on all devices is actually pretty tricky. What CSS properties should you animate? Should you use Javascript or CSS animations? What are hardware-accelerated animations? There are a lot of questions to answer, and we have to answer them, because if our animations are not performant, everything we learned so far will not create the desired effect.
Imagine if Sonner ran on 30 frames per second. Theo probably wouldn’t be as excited as he was when it ran at 60 frames per second.
Sonner running at 30 frames per second
## What are performant animations?
To keep it simple, performant animations are those that run at 60 frames per second, the same as most screen refresh rates. It means that we need to be able to re-render in ~16.7 milliseconds (1000/60). This is required in order for our brain to perceive motion as fluid.
## How do we make our animations performant?
Let’s start by explaining how animations work in the browser.
When we animate an element the browser needs to reflect this change. The steps a browser’s renderer takes to do this are:
- Layout: Calculate the size and position of elements on the page.
- Paint: Draw the page into graphical layers - essentially individual images that make up the page.
- Composite: Draw these layers to the viewport.
Animating properties like padding, margin, and height will cause the browser to recalculate the layout of the page, because if our element is shrinking or growing, it might affect the position of other elements. This recalculation is expensive and might cause our animation to drop frames.
Whether the animation will drop frames while using these properties depends on how much of the layout is affected. If the element has position: absolute or very few children, then we might get away with animating margin or padding.
The animation below is using padding to create this "scale" animation and it works fine on my device, but I can imagine that it might drop frames on a slower device.
Thankfully, we don’t have to take this risk. We can achieve the same animation by using scale in CSS and not worry about any of the potential issues mentioned above. 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. The less work the browser has to do, the better the performance.
Transforms don’t trigger layout recalculation as they only affect the visual representation of an element, not its position in the document flow.
The blue rectangle shows the actual DOM position of the animated element.
If we animated using margin instead, we’d see the text moving as well. This is an innocent example as we only move one text node, but imagine if we moved a whole dashboard view, by animating a sidebar, that’d be a lot of work for the browser.
## JS vs CSS animations
CSS is usually the better choice when we are talking strictly about performance, if it’s done right.
The problem with JS animations arises when they are using requestAnimationFrame to animate. Javascript code will always run on the main thread. This means that if the browser is busy doing something else, it might skip a frame, causing the animation to drop frames.
Some CSS and WAAPI animations are hardware-accelerated, which means that the browser can offload the work to the GPU. A hardware-accelerated animation will remain smooth, no matter how busy the main thread is. If you are animating using transform in CSS the animation will likely be hardware-accelerated.
The interactive demo below demonstrates how the business of the main thread affects animations. The more logos you add, the more work the browser has to do, and the more likely that the Framer Motion animation will drop frames, as it’s using requestAnimationFrame. CSS and WAAPI animations are hardware-accelerated and will remain smooth.
This example is inspired by the Sidebar Animation Performance post by Josh Wootonn.
This issue happened in the Vercel’s dashboard where we animated the active highlight of a 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.
## Transform shift
Sometimes one animation can be handled by both the CPU and the GPU, there’s a hand-off between the two. This can cause our animation to shift sometimes. To avoid this, we can use the will-change property to ensure that the animation will be handled exclusively by the GPU.
```css
.element {
/* This lets the browser know that this animation should be handled by the GPU. */
will-change: transform;
}
```
```css
.element {
/* This lets the browser know that this animation should be handled by the GPU. */
will-change: transform;
}
```
I was aware of this fix for a while, but Josh Comeau’s css-for-js course helped me understand why I’m adding this property, and that’s where this part comes from.
## Re-renders in React
When talking about animations in React we have to think about re-renders as well. Animations libraries like react-spring or Framer Motion animate things outside of React’s render cycle, but if you ever try to animate something in React that depends on state you might run into issues if you re-render too often.
Below we modify the y position of each logo directly through the style property, we do that roughly every 16.7 milliseconds via requestAnimationFrame. When we click on the button to unoptimize, we’ll switch to updating the state rather than modifying the style directly. This will cause a re-render on every frame, which will then cause the animation to drop frames.
## Hardware-accelerated animations in Framer Motion
Hardware acceleration is also available in Framer Motion actually, but you have to remember to animate transforms as a string. Most people animate using x and y values, which is more readable, but if you’d need to hardware accelerate the animation, you can keep this in mind.
## Exceptions
When I was building Vaul I ran into a performance issue that I couldn’t solve for days.
Once the content of the drawer got bigger than ~20 list items the drag gesture became laggy, and I couldn’t figure out why. Dragging was done without any re-renders, so what could cause it to drop frames? Every time the drag position gets updated, I changed a CSS variable which is then used as the value for translateY().
```js
const style = {
"--swipe-amount": `${draggedDistance}px`,
};
```
```js
const style = {
"--swipe-amount": `${draggedDistance}px`,
};
```
Since CSS Variables are inheritable, changing them will cause style recalculation for all children, meaning the more items I have in my drawer, the more expensive the calculation gets. Updating the style directly on the element fixed the issue. This seems like a quick fix in retrospect, but it took me hours to figure out.
```js
const style = {
transform: `translateY(${draggedDistance}px)`,
};
```
```js
const style = {
transform: `translateY(${draggedDistance}px)`,
};
```
Another exception is that blur animations become very laggy very quickly, especially on Safari. That’s why you should not use a higher value than ~20px for the blur filter.
## Does this mean I should avoid JS animations?
No. The problem with JS animations arises when your website is performing heavy processing during an animation. This rarely happens, and if it does, you now read this article and know how to fix or avoid it.
On top of that libraries like Framer Motion offer a lot more than CSS animations. You can use spring animations, shared layout animations, and more. Framer Motion does use requestAnimationFrame though and it’s a pretty big package, so you should really think about whether you need the features it offers.
Personally, I often times combine CSS animations with Framer Motion. CSS for simple animations and those that should be hardware-accelerated, and Framer Motion for more complex/sophisticated animations.
---
# The big little details
- Module: Good vs Great Animations
- Lesson URL: /lessons/good-vs-great-animations/the-big-little-details/
- Markdown URL: /md/lessons/good-vs-great-animations/the-big-little-details.md
- Source mirror path: apps/anim/learn/good-vs-great-animations/the-big-little-details.html
We’ve talked about the theory behind good animations and how to actually code them. Now it’s time to talk about the details that separate good animations from great animations. And we’ll start with feeling.
## Overview
We’ve talked about the theory behind good animations and how to actually code them. Now it’s time to talk about the details that separate good animations from great animations. And we’ll start with feeling.
## Feeling
Design has its rules for making a website appear luxurious, whimsical, or geeky. Using a serif font can make a website feel more premium, while using a monospace font indicates that the website is related to tech. Blue color is often used for trust, think of PayPal or Goldman Sachs. All of this helps us convey the feeling of the brand. But what about animations?
PayPal’s website is full of blue color, which is often associated with trust.
If we look at Stripe for example, we can see that their animations are pretty slow, they take their time. It feels premium and reliable to me, as if they are saying "we are not in a hurry, we are here for you". I’m talking strictly about the marketing website here, not the dashboard.
If you are a young, forward-thinking agency, you could go for more "edgy" ease-in-out curves.
When I worked at Vercel, I maintained our design system and we decided to make our animations very fast or in some cases even instant (no animation at all). Vercel is a company that is all about speed, so it made sense to make the product itself feel fast.
I wanted Sonner to feel premium and elegant. That’s why I made the animations feel a bit slower than usual. It also uses an ease curve rather than ease-out, it doesn’t feel as snappy, but it makes it feel more elegant, it fits the vibe I was going for.
Usually, our main goal should be to build interfaces that feel fast and are not annoying. Having slower animations on your marketing page is fine, as they are mostly informative and not interactive, and that’s where you should try and convey the feeling of your brand. But when it comes to the product itself, we should aim for speed. Family is the perfect example of that. I know that I mention Family a lot, but it just nails everything we talk about in this course.
## Orchestration
A "detail" that can take your animation to the next level. Visiting Paco’s site feels like a moment of joy. The enter animation is sequenced perfectly, it feels like a wave. It’s way nicer than if everything just animated in together.
Another great example of it is Apple’s navigation menu where they fade in the columns with a slight delay. This feels like a wave effect to me again. It’s hard to do achieve such effect, as the delay has to be just right.
I can’t give you any specific tips here, as I believe that this type of details are just a matter of trial and error until feels right. I told you in the taste lesson that you need to surround yourself with great work and practice. So let’s try, and recreate Paco’s enter animation!
Framer Motion makes orchestration easier with stagger. But let’s use pure CSS here so that you can see how it’s done without any libraries.
```text
import "./styles.css";
export default function Orchestration() {
return (
Jon Doe
{COPY.map((copy) => (
{copy}
))}
);
}
const COPY = [
`Using Apple's Sheet component on iOS feels natural, I wanted to create the same experience, but for the web. That's how Vaul, the React component was born.`,
`Open-sourcing meant that more people will use it, which will result in more feedback, ultimately making the component better.`,
`I chose to build Vaul on top of Radix's Dialog primitive. Radix ensures the component is accessible, handles focus management etc. I also made Vaul's API is very similar to Radix's, so that it feels familiar.`,
`Once the content of the drawer got bigger than ~20 list items the drag gesture became laggy, and I couldn't figure out why. `,
`Since CSS Variables are inheritable, changing them will cause style recalculation for all children, meaning the more items I have in my drawer, the more expensive the calculation gets.`,
];
```
## Blur
Blur is used more and more in web animations. It’s a nice way to create a better sense of motion and to mask any potential imperfections in the animation. We’ve used this in our feedback component for the success state.
The Dynamic Island that’ll build soon is another example of where blur makes a difference. You can try turning the blur on and off in the component below, and see how it affects the animation. You might not notice a huge change, but it definitely feels better with the blur on.
## Reviewing your work
I’ve spoken about it before, but I want to emphasize the importance of reviewing your work. The animations I post on twitter are never shared the day they are made. I give myself at least a day to look at them with fresh eyes. It’s the same as with design, after stepping away, you’ll notice things you didn’t see before.
Replay your animation, see how it moves, how it can be improved, step away from the code. Leonardo da Vinci would spend hours just staring at his work, not applying a single brushstroke. I’m obviously not comparing us to someone like da Vinci, but I do believe that people don’t give their animations enough time. They focus on the code, write it, and move on. The greatest animations take time, not only to code, but to review and improve it.
The transitions in Sonner were done in a few days, but I kept replaying them literally everyday until I published them. Who knows, maybe those additional few tweaks made Theo appreciate the animation the way he did.
---
# Click animation
- Module: Hero Illustration
- Lesson URL: /lessons/hero-illustration/click-animation/
- Markdown URL: /md/lessons/hero-illustration/click-animation.md
- Source mirror path: apps/anim/learn/hero-illustration/click-animation.html
In this lesson we’ll build the click animation. We’ll layer transform-based animations, path morphing, and stroke dash effects with careful timing to make everything feel unified.
## Overview
In this lesson we’ll build the click animation. We’ll layer transform-based animations, path morphing, and stroke dash effects with careful timing to make everything feel unified.
## What we’re working with
The hand SVG we’ll be working with has three main elements:
- Background: We’ll scale this when the hand clicks for extra feedback.
- Hand: A single element we’ll morph between resting and clicked shapes.
- Three lines: All elements are of the same length and will be animate with the strokeDashoffset method discussed in the previous lesson.
Notice the data-animate attributes on each element below. We’ll use these to target elements for animation without needing refs. The data-index attribute will be used to distinguish between the lines to select animation values and for staggered timing.
```jsx
```
```jsx
```
## Why useAnimate?
Before we dive in, let’s talk about why we’re using useAnimate instead of Motion’s declarative props like initial, animate, whileHover, etc.
Those work great for simple cases, but here we’re coordinating multiple SVG elements at once - pulse the background, move the hand, morph the hand shape, and draw in three motion lines with staggered timing. All triggered from different events (hover, click, idle timer) with precise coordination between them.
A declarative approach would require managing animation states:
```jsx
const [animationState, setAnimationState] = useState("idle"); // idle, hover, leave, click
const [canClick, setCanClick] = useState(false);
```
```jsx
const [animationState, setAnimationState] = useState("idle"); // idle, hover, leave, click
const [canClick, setCanClick] = useState(false);
```
Every state change triggers a re-render, even though we’re just updating animation phases.
Then there’s path morphing - there’s no declarative way to interpolate between SVG paths, so we’re forced into imperative code regardless:
```jsx
const handPathProgress = useMotionValue(0);
const handPath = useTransform(handPathProgress, [0, 1], handPaths);
useEffect(() => {
// Manually animate path for each state: hover, click, idle, leave...
if (animationState === "hover") {
animate(handPathProgress, [0, 1, 0], {
/* timing config */
});
}
// ... more state checks
}, [animationState]);
```
```jsx
const handPathProgress = useMotionValue(0);
const handPath = useTransform(handPathProgress, [0, 1], handPaths);
useEffect(() => {
// Manually animate path for each state: hover, click, idle, leave...
if (animationState === "hover") {
animate(handPathProgress, [0, 1, 0], {
/* timing config */
});
}
// ... more state checks
}, [animationState]);
```
Now we’re managing both declarative variants (for the background and lines) and imperative animations (for the path) simultaneously.
The biggest pain is coordinating completion. When the user’s mouse leaves, we need to wait for all the “leave” animations to finish before starting the idle loop. With declarative variants, there’s no built-in way to await completion across multiple elements, so we’d need completion tracking:
```jsx
const [leaveCompleted, setLeaveCompleted] = useState({
background: false,
hand: false,
lines: false,
});
// Add onAnimationComplete to every element
{
setLeaveCompleted((prev) => ({ ...prev, background: true }));
}}
/>;
// Then check when everything's done
useEffect(() => {
if (
animationState === "leave" &&
leaveCompleted.background &&
leaveCompleted.hand &&
leaveCompleted.lines
) {
setAnimationState("idle"); // Finally start idle loop
}
}, [animationState, leaveCompleted]);
```
```jsx
const [leaveCompleted, setLeaveCompleted] = useState({
background: false,
hand: false,
lines: false,
});
// Add onAnimationComplete to every element
{
setLeaveCompleted((prev) => ({ ...prev, background: true }));
}}
/>;
// Then check when everything's done
useEffect(() => {
if (
animationState === "leave" &&
leaveCompleted.background &&
leaveCompleted.hand &&
leaveCompleted.lines
) {
setAnimationState("idle"); // Finally start idle loop
}
}, [animationState, leaveCompleted]);
```
That’s a lot of ceremony for “wait for these animations to finish, then start the next one”.
With useAnimate, we get imperative control that lets us orchestrate everything from event handlers. We can coordinate timing, sequence animations, and wait for completion naturally - without the state management overhead. Let’s see how this works.
## Getting started
First, we need to convert our static SVG elements to motion components and import the useAnimate hook:
```jsx
import { useAnimate, motion } from "motion/react";
export default function ClickAnimation() {
const [scope, animate] = useAnimate();
return (
);
}
```
```jsx
import { useAnimate, motion } from "motion/react";
export default function ClickAnimation() {
const [scope, animate] = useAnimate();
return (
);
}
```
Notice how the background is wrapped in a motion.g (group), but the hand and lines are motion.path and motion.line directly. This is intentional.
Groups serve multiple purposes in SVG animations. The outer group wraps all our elements together so we can attach event handlers and apply animations to the entire illustration. The background gets its own group because it has a filter effect applied - we’ll animate the group rather than the path directly to keep the filter styling unchanged. The hand and lines can be animated directly since they don’t need this separation.
This pattern also lets you layer multiple animations on the same element by wrapping it in nested groups, where each group handles a different transform. We’ll see this in action in the next lessons.
The scope ref on the outer group defines the boundary for our animate() function - it tells Motion which part of the DOM to search when we use selectors.
## The background scale effect
Now let’s add the background transform animation. The animate function takes three arguments: a selector, animation values, and options where we can define our transition values.
```jsx
const [scope, animate] = useAnimate();
const handleMouseEnter = () => {
animate(
"[data-animate='background']",
{ transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"] },
{ duration: 0.53, ease: easeOut },
);
};
return (
);
```
```jsx
const [scope, animate] = useAnimate();
const handleMouseEnter = () => {
animate(
"[data-animate='background']",
{ transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"] },
{ duration: 0.53, ease: easeOut },
);
};
return (
);
```
Let’s break down what’s happening here.
We’re calling useAnimate which gives us a scope ref and an animate function. The ref gets attached to a parent element, creating a scope for our animations. Any element inside this scope can be targeted by our animate function.
When the mouse enters, we use animate with a CSS selector "[data-animate='background']" to find our background element. This is more flexible than using refs when you need to animate multiple elements.
The animation itself is a keyframe sequence. We pass an array of transform values creating compress → overshoot → settle.
We don’t need to explicitly define transform-box: fill-box style on the motion.g element as motion does this by default on SVG elements, and sets the transform-origin to 50% 50% (or center) which is exactly what we need here.
We also set overflow: visible on the SVG to make sure that the SVG doesn’t clip the background path when it overshoots the SVG bounds.
Hover over the background to see the animation.
## Moving the hand into position
Looking at the full animation, you’ll see the background scaling is delayed. It doesn’t happen when you hover - it syncs with the click for a more cohesive feel.
The hand needs to shift position to center the index finger with the middle line. Finding these values takes some trial and error - I used CSS transforms directly in the browser console until it felt right: translateX(-4px) translateY(3px) rotate(25deg).
We’ll also use the times option which let’s us change the timing of each keyframe. Each value in the times array is a value between 0 and 1, representing when that keyframe occurs during the animation’s duration. For example, with times: [0, 0.4] on a 1s animation, the second keyframe happens at 0.4s (40% through).
Now let’s add the transform keyframes and coordinate the animations:
```jsx
const handleMouseEnter = () => {
// Background: delayed start
animate(
"[data-animate='background']",
{ transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"] },
{
duration: 0.53,
ease: easeOut,
times: [0, 0.2, 0.6, 1],
delay: 0.2, // Syncs with hand path morphing
},
);
// Hand: moves into position
animate(
"[data-animate='hand']",
{
transform: [
"translateX(0px) translateY(0px) rotate(0deg)",
"translateX(-4px) translateY(3px) rotate(25deg)",
],
},
{
duration: 0.53,
times: [0, 0.4],
ease: easeInOut,
},
);
};
const handleMouseLeave = () => {
animate("[data-animate='hand']", {
transform: "translateX(0px) translateY(0px) rotate(0deg)",
});
};
```
```jsx
const handleMouseEnter = () => {
// Background: delayed start
animate(
"[data-animate='background']",
{ transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"] },
{
duration: 0.53,
ease: easeOut,
times: [0, 0.2, 0.6, 1],
delay: 0.2, // Syncs with hand path morphing
},
);
// Hand: moves into position
animate(
"[data-animate='hand']",
{
transform: [
"translateX(0px) translateY(0px) rotate(0deg)",
"translateX(-4px) translateY(3px) rotate(25deg)",
],
},
{
duration: 0.53,
times: [0, 0.4],
ease: easeInOut,
},
);
};
const handleMouseLeave = () => {
animate("[data-animate='hand']", {
transform: "translateX(0px) translateY(0px) rotate(0deg)",
});
};
```
The hand reaches its final position at 40% through the animation (times: [0, 0.4]), while the background starts later with a 0.2s delay. This layered timing, elements starting and finishing at different moments, makes the animation feel more organic than if everything moved in lockstep.
We also added a handleMouseLeave callback to reset the hand to its initial position when the mouse leaves the hand.
Hover over the hand below to see the effect of our implementation above using the times option.
## Path morphing: the clicking motion
To sell the “click” animation, we could take the easy route and scale the hand down. But with Motion’s useTransform, we can morph the SVG path itself - animating the index finger into a clicking position.
From the previous lesson, you know a path is a set of commands that draw a shape. useTransform can interpolate between two paths as long as they have the same structure and number of points. Our hand paths both have the same sequence of move and curve commands, making them perfect for simple interpolation.
If you needed to morph between very different paths (different number of points or command types), you’d use a library like flubber - here’s an example from Motion’s docs. But for our matching paths, useTransform is all we need.
The pattern for path morphing with Motion looks like this:
```jsx
const handPaths = [
"M58.5431 71.4419C60.2401...", // open hand
"M58.7957 70.4997C60.4927...", // clicking hand
];
const handPathProgress = useMotionValue(0);
const handPath = useTransform(handPathProgress, [0, 1], handPaths);
// In the animation:
animate(handPathProgress, [0, 1, 0], {
duration: 0.53,
times: [0.4, 0.6, 0.9],
ease: easeOut,
});
// In the JSX:
;
```
```jsx
const handPaths = [
"M58.5431 71.4419C60.2401...", // open hand
"M58.7957 70.4997C60.4927...", // clicking hand
];
const handPathProgress = useMotionValue(0);
const handPath = useTransform(handPathProgress, [0, 1], handPaths);
// In the animation:
animate(handPathProgress, [0, 1, 0], {
duration: 0.53,
times: [0.4, 0.6, 0.9],
ease: easeOut,
});
// In the JSX:
;
```
Here’s how this works. We create a useMotionValue for the animation progress, then useTransform converts that progress into interpolated SVG paths. We pass it the progress value, the input range [0, 1], and the two paths to morph between. When the progress value changes, the path automatically updates.
When we animate the progress from 0 → 1 → 0, we get open hand → clicking hand → open hand. The times array controls when these transitions happen - the hand starts morphing at 40% through the animation, reaches full curl at 60%, and returns to open by 90%.
```text
"use client";
import {
motion,
useAnimate,
useMotionValue,
useTransform,
easeOut,
easeInOut,
} from "motion/react";
const handPaths = [
"M58.5431 71.4419C60.2401 71.6239 61.3091 69.6649 60.2391 68.3369L49.2671 54.7189C48.6804 53.9554 48.4158 52.9927 48.5299 52.0366C48.6439 51.0805 49.1275 50.207 49.8773 49.6028C50.6271 48.9987 51.5835 48.712 52.542 48.804C53.5004 48.8959 54.3849 49.3593 55.0061 50.0949L58.3501 54.2449C58.8429 54.8569 59.5061 55.3089 60.2559 55.5437C61.0057 55.7785 61.8083 55.7855 62.5621 55.5639L70.6681 53.1809C72.8705 52.5335 75.23 52.6775 77.3372 53.5881C79.4444 54.4987 81.1663 56.1184 82.2041 58.1659L84.9681 63.6179C85.7915 65.2412 86.0321 67.098 85.6498 68.8775C85.2675 70.657 84.2855 72.2511 82.8681 73.3929L74.6611 80.0039C73.7139 80.7673 72.604 81.3028 71.417 81.5691C70.23 81.8355 68.9976 81.8255 67.8151 81.5399L55.1911 78.4919C54.2783 78.2707 53.4839 77.7106 52.9688 76.9252C52.4537 76.1399 52.2566 75.188 52.4174 74.2627C52.5781 73.3374 53.0848 72.5078 53.8346 71.9422C54.5844 71.3766 55.5212 71.1173 56.4551 71.2169L58.5431 71.4419Z",
"M58.7957 70.4997C60.4927 70.6817 61.3092 69.6653 60.2392 68.3373L51.8366 57.8368C51.2499 57.0733 50.867 55.9897 50.981 55.0336C51.0951 54.0776 51.5787 53.204 52.3285 52.5999C53.0783 51.9958 54.0347 51.7091 54.9931 51.801C55.9516 51.893 56.7918 52.4457 57.413 53.1813L58.3502 54.2453C58.8429 54.8573 59.5062 55.3093 60.256 55.5441C61.0058 55.7789 61.8084 55.7859 62.5622 55.5643L70.6681 53.1813C72.8705 52.5339 75.23 52.6779 77.3373 53.5885C79.4445 54.4991 81.1664 56.1187 82.2042 58.1663L84.9682 63.6183C85.7915 65.2416 86.0322 67.0984 85.6499 68.8778C85.2676 70.6573 84.2855 72.2515 82.8682 73.3933L74.6611 80.0043C73.714 80.7677 72.604 81.3032 71.417 81.5695C70.23 81.8358 68.9977 81.8258 67.8151 81.5403L55.5115 77.3318C54.5988 77.1105 53.8043 76.5504 53.2892 75.765C52.7742 74.9797 52.577 74.0279 52.7378 73.1025C52.8986 72.1772 53.4052 71.3476 54.155 70.782C54.9048 70.2165 55.8416 69.9572 56.7755 70.0567L58.7957 70.4997Z"
];
export default function ClickAnimation() {
const [scope, animate] = useAnimate();
const handPathProgress = useMotionValue(0);
const handPath = useTransform(handPathProgress, [0, 1], handPaths);
const handleMouseEnter = () => {
animate(
"[data-animate='background']",
{ transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"] },
{
duration: 0.55,
times: [0, 0.2, 0.6, 1],
ease: easeOut,
delay: 0.2,
}
);
animate(
"[data-animate='hand']",
{
transform: [
"translateX(0px) translateY(0px) rotate(0deg)",
"translateX(-4px) translateY(3px) rotate(25deg)",
],
},
{
duration: 0.55,
times: [0, 0.4],
ease: easeInOut,
}
);
animate(handPathProgress, [0, 1, 0], {
duration: 0.55,
times: [0.4, 0.6, 0.9],
ease: easeOut,
});
};
const handleMouseLeave = () => {
animate("[data-animate='hand']", {
transform: "translateX(0px) translateY(0px) rotate(0deg)",
});
};
return (
);
}
```
## Drawing the motion lines
Now we’ll animate the motion lines using the stroke dash technique from the previous lessons.
Here’s how we set up each line:
```jsx
```
```jsx
```
We’re normalizing the line length with pathLength="1" and creating a dashed pattern with strokeDasharray="1px 1.1px". Notice the gap (1.1px) is larger than the dash (1px).
This is because strokeLinecap="round" extends the caps (ends) beyond the mathematical dash length. If the gap matched the dash exactly, those rounded caps would peek through when the line should be fully hidden. The extra 0.1px ensures clean visibility control when we offset the pattern.
Now let’s get all the lines within the scope and animate them to their appropriate values:
```jsx
// Initial offsets create the staggered appearance
const defaultStrokeDashoffsets = [0, 0.55, 0.9];
const handleMouseEnter = () => {
// ...other animations
scope.current.querySelectorAll("[data-animate='line']").forEach((line) => {
const index = line.getAttribute("data-index");
animate(
line,
// Middle line (1) draws fully, outer lines stop at 60%
{ strokeDashoffset: index === "1" ? [1.05, 0] : [1.05, 0.4] },
{
delay: index === "1" ? 0 : 0.04, // Delay the outer lines
duration: 0.53,
times: [0.7, 0.9], // Quick reveal
}
);
});
};
const handleMouseLeave = () => {
// ...other animations
// Animate the lines back to their default positions
scope.current.querySelectorAll("[data-animate='line']").forEach((line) => {
const index = line.getAttribute("data-index");
animate(line, {
strokeDashoffset: defaultStrokeDashoffsets[Number(index)],
});
});
};
```
```jsx
// Initial offsets create the staggered appearance
const defaultStrokeDashoffsets = [0, 0.55, 0.9];
const handleMouseEnter = () => {
// ...other animations
scope.current.querySelectorAll("[data-animate='line']").forEach((line) => {
const index = line.getAttribute("data-index");
animate(
line,
// Middle line (1) draws fully, outer lines stop at 60%
{ strokeDashoffset: index === "1" ? [1.05, 0] : [1.05, 0.4] },
{
delay: index === "1" ? 0 : 0.04, // Delay the outer lines
duration: 0.53,
times: [0.7, 0.9], // Quick reveal
}
);
});
};
const handleMouseLeave = () => {
// ...other animations
// Animate the lines back to their default positions
scope.current.querySelectorAll("[data-animate='line']").forEach((line) => {
const index = line.getAttribute("data-index");
animate(line, {
strokeDashoffset: defaultStrokeDashoffsets[Number(index)],
});
});
};
```
Each line starts at a different offset (defined in defaultStrokeDashoffsets), creating that initial staggered appearance. On hover, we animate them from 1.05 (hidden) to their target positions - the middle line draws in fully (0), while the outer lines stop at 0.4, showing only 60% of their length.
The middle line starts immediately while the others have a 40ms delay, and all animations happen in 20% of the duration (times: [0.7, 0.9]). This creates a quick, punchy reveal that coincides with the hand path animation ending.
On mouse leave, each line returns to its initial offset position, ready for the next interaction.
```text
"use client";
import {
motion,
useAnimate,
useMotionValue,
useTransform,
easeOut,
easeInOut,
} from "motion/react";
const handPaths = [
"M58.5431 71.4419C60.2401 71.6239 61.3091 69.6649 60.2391 68.3369L49.2671 54.7189C48.6804 53.9554 48.4158 52.9927 48.5299 52.0366C48.6439 51.0805 49.1275 50.207 49.8773 49.6028C50.6271 48.9987 51.5835 48.712 52.542 48.804C53.5004 48.8959 54.3849 49.3593 55.0061 50.0949L58.3501 54.2449C58.8429 54.8569 59.5061 55.3089 60.2559 55.5437C61.0057 55.7785 61.8083 55.7855 62.5621 55.5639L70.6681 53.1809C72.8705 52.5335 75.23 52.6775 77.3372 53.5881C79.4444 54.4987 81.1663 56.1184 82.2041 58.1659L84.9681 63.6179C85.7915 65.2412 86.0321 67.098 85.6498 68.8775C85.2675 70.657 84.2855 72.2511 82.8681 73.3929L74.6611 80.0039C73.7139 80.7673 72.604 81.3028 71.417 81.5691C70.23 81.8355 68.9976 81.8255 67.8151 81.5399L55.1911 78.4919C54.2783 78.2707 53.4839 77.7106 52.9688 76.9252C52.4537 76.1399 52.2566 75.188 52.4174 74.2627C52.5781 73.3374 53.0848 72.5078 53.8346 71.9422C54.5844 71.3766 55.5212 71.1173 56.4551 71.2169L58.5431 71.4419Z",
"M58.7957 70.4997C60.4927 70.6817 61.3092 69.6653 60.2392 68.3373L51.8366 57.8368C51.2499 57.0733 50.867 55.9897 50.981 55.0336C51.0951 54.0776 51.5787 53.204 52.3285 52.5999C53.0783 51.9958 54.0347 51.7091 54.9931 51.801C55.9516 51.893 56.7918 52.4457 57.413 53.1813L58.3502 54.2453C58.8429 54.8573 59.5062 55.3093 60.256 55.5441C61.0058 55.7789 61.8084 55.7859 62.5622 55.5643L70.6681 53.1813C72.8705 52.5339 75.23 52.6779 77.3373 53.5885C79.4445 54.4991 81.1664 56.1187 82.2042 58.1663L84.9682 63.6183C85.7915 65.2416 86.0322 67.0984 85.6499 68.8778C85.2676 70.6573 84.2855 72.2515 82.8682 73.3933L74.6611 80.0043C73.714 80.7677 72.604 81.3032 71.417 81.5695C70.23 81.8358 68.9977 81.8258 67.8151 81.5403L55.5115 77.3318C54.5988 77.1105 53.8043 76.5504 53.2892 75.765C52.7742 74.9797 52.577 74.0279 52.7378 73.1025C52.8986 72.1772 53.4052 71.3476 54.155 70.782C54.9048 70.2165 55.8416 69.9572 56.7755 70.0567L58.7957 70.4997Z"
];
const defaultStrokeDashoffsets = [0, 0.55, 0.9];
export default function ClickAnimation() {
const [scope, animate] = useAnimate();
const handPathProgress = useMotionValue(0);
const handPath = useTransform(handPathProgress, [0, 1], handPaths);
const handleMouseEnter = () => {
animate(
"[data-animate='background']",
{ transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"] },
{
duration: 0.53,
times: [0, 0.2, 0.6, 1],
ease: easeOut,
delay: 0.2,
}
);
animate(
"[data-animate='hand']",
{
transform: [
"translateX(0px) translateY(0px) rotate(0deg)",
"translateX(-4px) translateY(3px) rotate(25deg)",
],
},
{
duration: 0.53,
times: [0, 0.4],
ease: easeInOut,
}
);
animate(handPathProgress, [0, 1, 0], {
duration: 0.53,
times: [0.4, 0.6, 0.9],
ease: easeOut,
});
scope.current.querySelectorAll("[data-animate='line']").forEach((line) => {
const index = line.getAttribute("data-index");
animate(line, {
strokeDashoffset: index === "1" ? [1.05, 0] : [1.05, 0.4],
}, {
delay: index === "1" ? 0 : 0.04,
duration: 0.53,
times: [0.7, 0.9],
});
});
};
const handleMouseLeave = () => {
animate("[data-animate='hand']", {
transform: "translateX(0px) translateY(0px) rotate(0deg)",
});
scope.current.querySelectorAll("[data-animate='line']").forEach((line) => {
const index = line.getAttribute("data-index");
animate(line, {
strokeDashoffset: defaultStrokeDashoffsets[Number(index)],
});
});
};
return (
);
}
```
## Making it clickable
Let’s make this actually clickable. After the hover animation completes, clicking the hand should trigger the animations immediately without the gradual buildup.
The click handler is similar to hover, but with different timing:
```jsx
const handleClick = () => {
// Background: no delay on click (instant feedback)
animate(
"[data-animate='background']",
{ transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"] },
{
duration: 0.53,
times: [0.1, 0.3, 0.65, 1],
ease: easeOut,
}
);
// Hand: already in position from hover, just maintain it
animate("[data-animate='hand']", {
transform: "translateX(-4px) translateY(3px) rotate(25deg)",
});
// Path morph
animate(handPathProgress, [0, 1, 0], {
duration: 0.53,
times: [0.1, 0.3, 0.6],
ease: easeOut,
});
scope.current.querySelectorAll("[data-animate='line']").forEach((line) => {
const index = Number(line.getAttribute("data-index"));
animate(
line,
{
strokeDashoffset: index === 1 ? [1.05, 0] : [1.05, 0.4],
},
{
delay: index === 1 ? 0 : 0.04,
duration: 0.53,
times: [0.4, 0.6],
}
);
});
};
```
```jsx
const handleClick = () => {
// Background: no delay on click (instant feedback)
animate(
"[data-animate='background']",
{ transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"] },
{
duration: 0.53,
times: [0.1, 0.3, 0.65, 1],
ease: easeOut,
}
);
// Hand: already in position from hover, just maintain it
animate("[data-animate='hand']", {
transform: "translateX(-4px) translateY(3px) rotate(25deg)",
});
// Path morph
animate(handPathProgress, [0, 1, 0], {
duration: 0.53,
times: [0.1, 0.3, 0.6],
ease: easeOut,
});
scope.current.querySelectorAll("[data-animate='line']").forEach((line) => {
const index = Number(line.getAttribute("data-index"));
animate(
line,
{
strokeDashoffset: index === 1 ? [1.05, 0] : [1.05, 0.4],
},
{
delay: index === 1 ? 0 : 0.04,
duration: 0.53,
times: [0.4, 0.6],
}
);
});
};
```
Notice the differences from hover.
There’s no delay on the background - the click should feel instant and responsive.
For the hand transform, we pass a single value instead of keyframes. Since the hand is already in position from the hover, we just keep it there.
The path morph uses different times - starting at 0.1 instead of 0.4 to make the finger curl happen sooner. The proportions within the animation stay the same (20% to curl, 30% to uncurl), just happening earlier in the sequence.
Now, to make sure clicks only work after the hover animation finished, we can use a ref to track when it completes. This way we don’t trigger any re-renders as we’re not accessing the value inside JSX, so no need to reach for useState with this setup.
```jsx
const hasAnimationCompletedRef = useRef(false);
const handleMouseEnter = async () => {
// ...other animations
await animate(handPathProgress, [0, 1, 0], {
duration: 0.53,
times: [0.4, 0.6, 0.9],
ease: easeOut,
});
hasAnimationCompletedRef.current = true;
};
const handleMouseLeave = () => {
hasAnimationCompletedRef.current = false;
// ...animations
};
const handleClick = () => {
if (!hasAnimationCompletedRef.current) return;
// ...animations
};
```
```jsx
const hasAnimationCompletedRef = useRef(false);
const handleMouseEnter = async () => {
// ...other animations
await animate(handPathProgress, [0, 1, 0], {
duration: 0.53,
times: [0.4, 0.6, 0.9],
ease: easeOut,
});
hasAnimationCompletedRef.current = true;
};
const handleMouseLeave = () => {
hasAnimationCompletedRef.current = false;
// ...animations
};
const handleClick = () => {
if (!hasAnimationCompletedRef.current) return;
// ...animations
};
```
We make handleMouseEnter async and await one of the animations. Since they all have the same duration, any one works. Once it completes, we set the ref to true.
We reset it to false in handleMouseLeave, and guard against early clicks in handleClick.
Alternative approaches include await Promise.all([...animations]) if durations differ, or using .then() notation. Pick what fits your style or animation setup.
```text
"use client";
import { useRef } from "react";
import {
motion,
useAnimate,
useMotionValue,
useTransform,
easeOut,
easeInOut,
} from "motion/react";
const handPaths = [
"M58.5431 71.4419C60.2401 71.6239 61.3091 69.6649 60.2391 68.3369L49.2671 54.7189C48.6804 53.9554 48.4158 52.9927 48.5299 52.0366C48.6439 51.0805 49.1275 50.207 49.8773 49.6028C50.6271 48.9987 51.5835 48.712 52.542 48.804C53.5004 48.8959 54.3849 49.3593 55.0061 50.0949L58.3501 54.2449C58.8429 54.8569 59.5061 55.3089 60.2559 55.5437C61.0057 55.7785 61.8083 55.7855 62.5621 55.5639L70.6681 53.1809C72.8705 52.5335 75.23 52.6775 77.3372 53.5881C79.4444 54.4987 81.1663 56.1184 82.2041 58.1659L84.9681 63.6179C85.7915 65.2412 86.0321 67.098 85.6498 68.8775C85.2675 70.657 84.2855 72.2511 82.8681 73.3929L74.6611 80.0039C73.7139 80.7673 72.604 81.3028 71.417 81.5691C70.23 81.8355 68.9976 81.8255 67.8151 81.5399L55.1911 78.4919C54.2783 78.2707 53.4839 77.7106 52.9688 76.9252C52.4537 76.1399 52.2566 75.188 52.4174 74.2627C52.5781 73.3374 53.0848 72.5078 53.8346 71.9422C54.5844 71.3766 55.5212 71.1173 56.4551 71.2169L58.5431 71.4419Z",
"M58.7957 70.4997C60.4927 70.6817 61.3092 69.6653 60.2392 68.3373L51.8366 57.8368C51.2499 57.0733 50.867 55.9897 50.981 55.0336C51.0951 54.0776 51.5787 53.204 52.3285 52.5999C53.0783 51.9958 54.0347 51.7091 54.9931 51.801C55.9516 51.893 56.7918 52.4457 57.413 53.1813L58.3502 54.2453C58.8429 54.8573 59.5062 55.3093 60.256 55.5441C61.0058 55.7789 61.8084 55.7859 62.5622 55.5643L70.6681 53.1813C72.8705 52.5339 75.23 52.6779 77.3373 53.5885C79.4445 54.4991 81.1664 56.1187 82.2042 58.1663L84.9682 63.6183C85.7915 65.2416 86.0322 67.0984 85.6499 68.8778C85.2676 70.6573 84.2855 72.2515 82.8682 73.3933L74.6611 80.0043C73.714 80.7677 72.604 81.3032 71.417 81.5695C70.23 81.8358 68.9977 81.8258 67.8151 81.5403L55.5115 77.3318C54.5988 77.1105 53.8043 76.5504 53.2892 75.765C52.7742 74.9797 52.577 74.0279 52.7378 73.1025C52.8986 72.1772 53.4052 71.3476 54.155 70.782C54.9048 70.2165 55.8416 69.9572 56.7755 70.0567L58.7957 70.4997Z"
];
const defaultStrokeDashoffsets = [0, 0.55, 0.9];
export default function ClickAnimation() {
const [scope, animate] = useAnimate();
const handPathProgress = useMotionValue(0);
const handPath = useTransform(handPathProgress, [0, 1], handPaths);
const hasAnimationCompletedRef = useRef(false);
const handleMouseEnter = async () => {
animate(
"[data-animate='background']",
{ transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"] },
{
duration: 0.53,
times: [0, 0.2, 0.6, 1],
ease: easeOut,
delay: 0.2,
}
);
animate(
"[data-animate='hand']",
{
transform: [
"translateX(0px) translateY(0px) rotate(0deg)",
"translateX(-4px) translateY(3px) rotate(25deg)",
],
},
{
duration: 0.53,
times: [0, 0.4],
ease: easeInOut,
}
);
scope.current.querySelectorAll("[data-animate='line']").forEach((line) => {
const index = line.getAttribute("data-index");
animate(line, {
strokeDashoffset: index === "1" ? [1.05, 0] : [1.05, 0.4],
}, {
delay: index === "1" ? 0 : 0.04,
duration: 0.53,
times: [0.7, 0.9],
});
});
await animate(handPathProgress, [0, 1, 0], {
duration: 0.53,
times: [0.4, 0.6, 0.9],
ease: easeOut,
});
hasAnimationCompletedRef.current = true;
};
const handleMouseLeave = async () => {
hasAnimationCompletedRef.current = false;
animate("[data-animate='hand']", {
transform: "translateX(0px) translateY(0px) rotate(0deg)",
});
await Promise.all(
Array.from(scope.current.querySelectorAll("[data-animate='line']")).map((line) => {
const index = line.getAttribute("data-index");
return animate(line, {
strokeDashoffset: defaultStrokeDashoffsets[Number(index)],
});
})
);
};
const handleClick = () => {
if (!hasAnimationCompletedRef.current) return;
animate(
"[data-animate='background']",
{ transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"] },
{
duration: 0.53,
times: [0.1, 0.3, 0.65, 1],
ease: easeOut,
}
);
animate("[data-animate='hand']", {
transform: "translateX(-4px) translateY(3px) rotate(25deg)",
});
animate(handPathProgress, [0, 1, 0], {
duration: 0.53,
times: [0.1, 0.3, 0.6],
ease: easeOut,
});
scope.current.querySelectorAll("[data-animate='line']").forEach((line) => {
const index = Number(line.getAttribute("data-index"));
animate(
line,
{
strokeDashoffset: index === 1 ? [1.05, 0] : [1.05, 0.4],
},
{
delay: index === 1 ? 0 : 0.04,
duration: 0.53,
times: [0.4, 0.6],
},
);
});
};
return (
);
}
```
When users aren’t interacting with the animation, we want it to subtly loop to catch their attention. This means starting an infinite animation on mount, then restarting it when the user’s mouse leaves.
Here’s how we set up a repeating idle animation:
```jsx
const startIdleAnimations = () => {
// Background pulse
animate(
"[data-animate='background']",
{ transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"] },
{
duration: 0.63,
times: [0.2, 0.5, 0.85, 1],
ease: easeOut,
repeat: Infinity,
repeatType: "loop",
repeatDelay: 2,
delay: 2, // Initial delay before first loop
}
);
// Hand path morphing
animate(handPathProgress, [0, 1, 0], {
duration: 0.63,
times: [0.1, 0.4, 0.8],
ease: easeOut,
repeat: Infinity,
repeatType: "loop",
repeatDelay: 2,
delay: 2,
});
// Motion lines: hidden then back to default state
scope.current.querySelectorAll("[data-animate='line']").forEach((line) => {
const index = Number(line.getAttribute("data-index"));
animate(
line,
{
strokeDashoffset: [
defaultStrokeDashoffsets[index], // Start at default position
1.05, // Hidden
defaultStrokeDashoffsets[index], // Back to default
],
},
{
duration: 0.63,
// First 2 keyframes have the same time so that the line starts
// at it’s default position and is hidden instantly,
// then animates back to default position by 0.8
times: [0.5, 0.5, 0.8],
repeat: Infinity,
repeatType: "loop",
repeatDelay: 2,
delay: 2,
}
);
});
};
```
```jsx
const startIdleAnimations = () => {
// Background pulse
animate(
"[data-animate='background']",
{ transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"] },
{
duration: 0.63,
times: [0.2, 0.5, 0.85, 1],
ease: easeOut,
repeat: Infinity,
repeatType: "loop",
repeatDelay: 2,
delay: 2, // Initial delay before first loop
}
);
// Hand path morphing
animate(handPathProgress, [0, 1, 0], {
duration: 0.63,
times: [0.1, 0.4, 0.8],
ease: easeOut,
repeat: Infinity,
repeatType: "loop",
repeatDelay: 2,
delay: 2,
});
// Motion lines: hidden then back to default state
scope.current.querySelectorAll("[data-animate='line']").forEach((line) => {
const index = Number(line.getAttribute("data-index"));
animate(
line,
{
strokeDashoffset: [
defaultStrokeDashoffsets[index], // Start at default position
1.05, // Hidden
defaultStrokeDashoffsets[index], // Back to default
],
},
{
duration: 0.63,
// First 2 keyframes have the same time so that the line starts
// at it’s default position and is hidden instantly,
// then animates back to default position by 0.8
times: [0.5, 0.5, 0.8],
repeat: Infinity,
repeatType: "loop",
repeatDelay: 2,
delay: 2,
}
);
});
};
```
The key properties here are repeat: Infinity to loop forever, and repeatDelay: 2 to wait 2 seconds between loops.
We also use delay: 2 for the initial delay. This gives users time to discover the hover interaction naturally before the idle animation starts drawing attention. (Note that I made the delay and repeatDelay short here so you don’t have to wait too long to see it happen. In the hero animation we have repeatDelay: 6, delay: 2.5 on the first idle animation and delay: 6 for all subsequent idle animations.)
Now we need to start these animations when the component mounts:
```jsx
useEffect(() => {
startIdleAnimations();
}, []);
```
```jsx
useEffect(() => {
startIdleAnimations();
}, []);
```
Finally, we need to restart the idle animations when the user’s mouse leaves. But there’s a coordination challenge - if we start the idle loop while the hand and lines are still resetting to their initial positions, the animations will conflict.
The solution is to wait for the reset animations to complete before starting the idle loop:
```jsx
const handleMouseLeave = async () => {
hasAnimationCompletedRef.current = false;
animate("[data-animate='hand']", {
transform: "translateX(0px) translateY(0px) rotate(0deg)",
});
await Promise.all(
Array.from(scope.current.querySelectorAll("[data-animate='line']")).map(
(line) => {
const index = line.getAttribute("data-index");
return animate(line, {
strokeDashoffset: defaultStrokeDashoffsets[Number(index)],
});
}
)
);
startIdleAnimations();
};
```
```jsx
const handleMouseLeave = async () => {
hasAnimationCompletedRef.current = false;
animate("[data-animate='hand']", {
transform: "translateX(0px) translateY(0px) rotate(0deg)",
});
await Promise.all(
Array.from(scope.current.querySelectorAll("[data-animate='line']")).map(
(line) => {
const index = line.getAttribute("data-index");
return animate(line, {
strokeDashoffset: defaultStrokeDashoffsets[Number(index)],
});
}
)
);
startIdleAnimations();
};
```
We use Promise.all with .map() to wait for all line animations to complete before calling startIdleAnimations(). This ensures a smooth transition from the interactive state back to the idle loop.
```text
"use client";
import { useRef, useEffect } from "react";
import {
motion,
useAnimate,
useMotionValue,
useTransform,
easeOut,
easeInOut,
} from "motion/react";
const handPaths = [
"M58.5431 71.4419C60.2401 71.6239 61.3091 69.6649 60.2391 68.3369L49.2671 54.7189C48.6804 53.9554 48.4158 52.9927 48.5299 52.0366C48.6439 51.0805 49.1275 50.207 49.8773 49.6028C50.6271 48.9987 51.5835 48.712 52.542 48.804C53.5004 48.8959 54.3849 49.3593 55.0061 50.0949L58.3501 54.2449C58.8429 54.8569 59.5061 55.3089 60.2559 55.5437C61.0057 55.7785 61.8083 55.7855 62.5621 55.5639L70.6681 53.1809C72.8705 52.5335 75.23 52.6775 77.3372 53.5881C79.4444 54.4987 81.1663 56.1184 82.2041 58.1659L84.9681 63.6179C85.7915 65.2412 86.0321 67.098 85.6498 68.8775C85.2675 70.657 84.2855 72.2511 82.8681 73.3929L74.6611 80.0039C73.7139 80.7673 72.604 81.3028 71.417 81.5691C70.23 81.8355 68.9976 81.8255 67.8151 81.5399L55.1911 78.4919C54.2783 78.2707 53.4839 77.7106 52.9688 76.9252C52.4537 76.1399 52.2566 75.188 52.4174 74.2627C52.5781 73.3374 53.0848 72.5078 53.8346 71.9422C54.5844 71.3766 55.5212 71.1173 56.4551 71.2169L58.5431 71.4419Z",
"M58.7957 70.4997C60.4927 70.6817 61.3092 69.6653 60.2392 68.3373L51.8366 57.8368C51.2499 57.0733 50.867 55.9897 50.981 55.0336C51.0951 54.0776 51.5787 53.204 52.3285 52.5999C53.0783 51.9958 54.0347 51.7091 54.9931 51.801C55.9516 51.893 56.7918 52.4457 57.413 53.1813L58.3502 54.2453C58.8429 54.8573 59.5062 55.3093 60.256 55.5441C61.0058 55.7789 61.8084 55.7859 62.5622 55.5643L70.6681 53.1813C72.8705 52.5339 75.23 52.6779 77.3373 53.5885C79.4445 54.4991 81.1664 56.1187 82.2042 58.1663L84.9682 63.6183C85.7915 65.2416 86.0322 67.0984 85.6499 68.8778C85.2676 70.6573 84.2855 72.2515 82.8682 73.3933L74.6611 80.0043C73.714 80.7677 72.604 81.3032 71.417 81.5695C70.23 81.8358 68.9977 81.8258 67.8151 81.5403L55.5115 77.3318C54.5988 77.1105 53.8043 76.5504 53.2892 75.765C52.7742 74.9797 52.577 74.0279 52.7378 73.1025C52.8986 72.1772 53.4052 71.3476 54.155 70.782C54.9048 70.2165 55.8416 69.9572 56.7755 70.0567L58.7957 70.4997Z"
];
const defaultStrokeDashoffsets = [0, 0.55, 0.9];
export default function ClickAnimation() {
const [scope, animate] = useAnimate();
const handPathProgress = useMotionValue(0);
const handPath = useTransform(handPathProgress, [0, 1], handPaths);
const hasAnimationCompletedRef = useRef(false);
const startIdleAnimations = () => {
animate(
"[data-animate='background']",
{ transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"] },
{
duration: 0.63,
times: [0.2, 0.5, 0.85, 1],
ease: easeOut,
repeat: Infinity,
repeatType: "loop",
repeatDelay: 2,
delay: 2,
},
);
animate(handPathProgress, [0, 1, 0], {
duration: 0.63,
times: [0.1, 0.4, 0.8],
ease: easeOut,
repeat: Infinity,
repeatType: "loop",
repeatDelay: 2,
delay: 2,
});
scope.current.querySelectorAll("[data-animate='line']").forEach((line) => {
const index = Number(line.getAttribute("data-index"));
animate(
line,
{
strokeDashoffset: [
defaultStrokeDashoffsets[index],
1.05,
defaultStrokeDashoffsets[index],
],
},
{
duration: 0.63,
times: [0.5, 0.5, 0.8],
repeat: Infinity,
repeatType: "loop",
repeatDelay: 2,
delay: 2,
},
);
});
};
useEffect(() => {
startIdleAnimations();
}, []);
const handleMouseEnter = async () => {
animate(
"[data-animate='background']",
{ transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"] },
{
duration: 0.53,
times: [0, 0.2, 0.6, 1],
ease: easeOut,
delay: 0.2,
}
);
animate(
"[data-animate='hand']",
{
transform: [
"translateX(0px) translateY(0px) rotate(0deg)",
"translateX(-4px) translateY(3px) rotate(25deg)",
],
},
{
duration: 0.53,
times: [0, 0.4],
ease: easeInOut,
}
);
scope.current.querySelectorAll("[data-animate='line']").forEach((line) => {
const index = line.getAttribute("data-index");
animate(line, {
strokeDashoffset: index === "1" ? [1.05, 0] : [1.05, 0.4],
}, {
delay: index === "1" ? 0 : 0.04,
duration: 0.53,
times: [0.7, 0.9],
});
});
await animate(handPathProgress, [0, 1, 0], {
duration: 0.53,
times: [0.4, 0.6, 0.9],
ease: easeOut,
});
hasAnimationCompletedRef.current = true;
};
const handleMouseLeave = async () => {
hasAnimationCompletedRef.current = false;
animate("[data-animate='hand']", {
transform: "translateX(0px) translateY(0px) rotate(0deg)",
});
await Promise.all(
Array.from(scope.current.querySelectorAll("[data-animate='line']")).map((line) => {
const index = line.getAttribute("data-index");
return animate(line, {
strokeDashoffset: defaultStrokeDashoffsets[Number(index)],
});
})
);
startIdleAnimations();
};
const handleClick = () => {
if (!hasAnimationCompletedRef.current) return;
animate(
"[data-animate='background']",
{ transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"] },
{
duration: 0.53,
times: [0.1, 0.3, 0.65, 1],
ease: easeOut,
}
);
animate("[data-animate='hand']", {
transform: "translateX(-4px) translateY(3px) rotate(25deg)",
});
animate(handPathProgress, [0, 1, 0], {
duration: 0.53,
times: [0.1, 0.3, 0.6],
ease: easeOut,
});
scope.current.querySelectorAll("[data-animate='line']").forEach((line) => {
const index = Number(line.getAttribute("data-index"));
animate(
line,
{
strokeDashoffset: index === 1 ? [1.05, 0] : [1.05, 0.4],
},
{
delay: index === 1 ? 0 : 0.04,
duration: 0.53,
times: [0.4, 0.6],
},
);
});
};
return (
);
}
```
## Refactoring to variants
At this point, our animation works, but the code is getting messy. We have timing values scattered across event handlers, duplicated animation calls, and it’s hard to see at a glance how the hover animation differs from the click animation.
## Setting up the variants
Let’s clean this up using a variant pattern - defining all our animation states upfront in a centralized place.
Here’s what the background variants look like:
```jsx
// variants.js
const DURATION = 0.53;
export const backgroundVariants = {
initial: {
transform: "scale(1)",
},
hover: {
transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"],
transition: {
duration: DURATION,
times: [0, 0.2, 0.6, 1],
ease: easeOut,
delay: 0.2,
},
},
idle: {
transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"],
transition: {
// ...
},
},
click: {
transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"],
transition: {
// ...
},
},
};
// ...other variants
```
```jsx
// variants.js
const DURATION = 0.53;
export const backgroundVariants = {
initial: {
transform: "scale(1)",
},
hover: {
transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"],
transition: {
duration: DURATION,
times: [0, 0.2, 0.6, 1],
ease: easeOut,
delay: 0.2,
},
},
idle: {
transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"],
transition: {
// ...
},
},
click: {
transform: ["scale(1)", "scale(0.97)", "scale(1.01)", "scale(1)"],
transition: {
// ...
},
},
};
// ...other variants
```
Each variant contains both the animation values and transition config. Now when we want to understand how the idle animation differs from hover, we can see it at a glance.
For elements that need per-item customization, we use functions:
```jsx
export const lineVariants = {
initial: (i) => ({
strokeDashoffset: defaultStrokeDashoffsets[i],
}),
hover: (i) => ({
strokeDashoffset: i === 1 ? [1.05, 0] : [1.05, 0.4],
transition: {
delay: i === 1 ? 0 : 0.04,
duration: DURATION,
times: [0.7, 0.9],
},
}),
// ... other states
};
```
```jsx
export const lineVariants = {
initial: (i) => ({
strokeDashoffset: defaultStrokeDashoffsets[i],
}),
hover: (i) => ({
strokeDashoffset: i === 1 ? [1.05, 0] : [1.05, 0.4],
transition: {
delay: i === 1 ? 0 : 0.04,
duration: DURATION,
times: [0.7, 0.9],
},
}),
// ... other states
};
```
This keeps the per-line differences organized and easy to modify.
## A reusable pattern
We can also create a hook that wraps useAnimate and extracts transition config:
```jsx
// use-animate-variant.js
export function useAnimateVariant() {
const [scope, animate] = useAnimate();
const animateVariant = useCallback(
(selector, variant) => {
if (!variant) return;
const { transition, ...values } = variant;
return animate(
selector,
values,
transition ?? {
type: "spring",
stiffness: 800,
damping: 80,
mass: 4,
}
);
},
[animate]
);
return [scope, animateVariant, animate];
}
```
```jsx
// use-animate-variant.js
export function useAnimateVariant() {
const [scope, animate] = useAnimate();
const animateVariant = useCallback(
(selector, variant) => {
if (!variant) return;
const { transition, ...values } = variant;
return animate(
selector,
values,
transition ?? {
type: "spring",
stiffness: 800,
damping: 80,
mass: 4,
}
);
},
[animate]
);
return [scope, animateVariant, animate];
}
```
This separates transition config from values and provides a fallback transition for any animations without explicit timing. The return values are [scope, animateVariant, animate], so we keep the same motion convention, but prioritize our use-case, making it simpler to reuse the logic in other components.
So we can now do this:
```jsx
const [scope, animateVariant, animate] = useAnimateVariant();
const playAnimationState = (variant) => {
// Simple usage
animateVariant('[data-animate="background"]', backgroundVariants[variant]);
// ...other animations
};
```
```jsx
const [scope, animateVariant, animate] = useAnimateVariant();
const playAnimationState = (variant) => {
// Simple usage
animateVariant('[data-animate="background"]', backgroundVariants[variant]);
// ...other animations
};
```
For our click animation, though, we need to coordinate multiple elements and handle path morphing. Let’s build an orchestrator for that.
## Building the orchestrator
Now let’s build an orchestrator that plays all element animations for a given state:
```jsx
const [scope, animateVariant, animate] = useAnimateVariant();
const handPathAnimationRef = useRef(null);
const playAnimationState = async (variant, pathConfig) => {
// Cleanup: stop any in-flight animations to prevent conflicts
handPathAnimationRef.current?.stop();
if (!pathConfig?.keyframes) {
await animate(handPathProgress, 0);
}
// Collect all animations to coordinate with Promise.all
const animations = [];
// Animate background
animations.push(
animateVariant('[data-animate="background"]', backgroundVariants[variant])
);
// Animate hand position (skip for idle state)
if (variant !== "idle") {
animations.push(
animateVariant('[data-animate="hand"]', handVariants[variant])
);
}
// Animate all three lines with per-line variants
animations.push(
...Array.from({ length: 3 }, (_, i) => {
return animateVariant(
`[data-animate="line"][data-index='${i}']`,
lineVariants[variant](i)
);
})
);
// Handle path morphing separately (pathConfig for granular control)
if (pathConfig) {
const pathAnimation = animate(handPathProgress, pathConfig.keyframes, {
duration: pathConfig.repeat ? IDLE_DURATION : DURATION,
times: pathConfig.times,
ease: easeOut,
...(pathConfig.repeat && {
repeat: Infinity,
repeatType: "loop",
repeatDelay: REPEAT_DELAY,
delay: REPEAT_DELAY,
}),
});
handPathAnimationRef.current = pathAnimation;
}
// Return all animations so caller can await completion
return Promise.all(animations);
};
```
```jsx
const [scope, animateVariant, animate] = useAnimateVariant();
const handPathAnimationRef = useRef(null);
const playAnimationState = async (variant, pathConfig) => {
// Cleanup: stop any in-flight animations to prevent conflicts
handPathAnimationRef.current?.stop();
if (!pathConfig?.keyframes) {
await animate(handPathProgress, 0);
}
// Collect all animations to coordinate with Promise.all
const animations = [];
// Animate background
animations.push(
animateVariant('[data-animate="background"]', backgroundVariants[variant])
);
// Animate hand position (skip for idle state)
if (variant !== "idle") {
animations.push(
animateVariant('[data-animate="hand"]', handVariants[variant])
);
}
// Animate all three lines with per-line variants
animations.push(
...Array.from({ length: 3 }, (_, i) => {
return animateVariant(
`[data-animate="line"][data-index='${i}']`,
lineVariants[variant](i)
);
})
);
// Handle path morphing separately (pathConfig for granular control)
if (pathConfig) {
const pathAnimation = animate(handPathProgress, pathConfig.keyframes, {
duration: pathConfig.repeat ? IDLE_DURATION : DURATION,
times: pathConfig.times,
ease: easeOut,
...(pathConfig.repeat && {
repeat: Infinity,
repeatType: "loop",
repeatDelay: REPEAT_DELAY,
delay: REPEAT_DELAY,
}),
});
handPathAnimationRef.current = pathAnimation;
}
// Return all animations so caller can await completion
return Promise.all(animations);
};
```
This function stops any in-flight animations to prevent conflicts, resets the hand path when needed, collects all element animations into an array, and handles path morphing timing. We return the animations array inside a Promise.all so we can await the animation completion if we need to.
## The payoff
Now our event handlers become much simpler and easier to read:
```jsx
const startIdleAnimations = async () => {
await playAnimationState("idle", {
keyframes: [0, 1, 0],
times: [0.1, 0.4, 0.8],
repeat: true,
});
};
const handleMouseEnter = async () => {
await playAnimationState("hover", {
keyframes: [0, 1, 0],
times: [0.4, 0.6, 0.9],
});
hasAnimationCompletedRef.current = true;
};
const handleMouseLeave = async () => {
hasAnimationCompletedRef.current = false;
await playAnimationState("initial");
startIdleAnimations();
};
const handleClick = () => {
if (!hasAnimationCompletedRef.current) return;
playAnimationState("click", {
keyframes: [0, 1, 0],
times: [0.1, 0.3, 0.6],
});
};
```
```jsx
const startIdleAnimations = async () => {
await playAnimationState("idle", {
keyframes: [0, 1, 0],
times: [0.1, 0.4, 0.8],
repeat: true,
});
};
const handleMouseEnter = async () => {
await playAnimationState("hover", {
keyframes: [0, 1, 0],
times: [0.4, 0.6, 0.9],
});
hasAnimationCompletedRef.current = true;
};
const handleMouseLeave = async () => {
hasAnimationCompletedRef.current = false;
await playAnimationState("initial");
startIdleAnimations();
};
const handleClick = () => {
if (!hasAnimationCompletedRef.current) return;
playAnimationState("click", {
keyframes: [0, 1, 0],
times: [0.1, 0.3, 0.6],
});
};
```
Just a single function call per interaction. The intent is crystal clear.
The benefits of this refactor:
- All timing values live in one file - want to make hover snappier? Edit one variant object.
- Each animation state is self-contained, so you can see exactly what happens during "hover" without tracing through multiple functions.
- The orchestrator handles common patterns once instead of repeating them in every handler.
- And as animations grow more complex, this pattern scales much better.
```text
"use client";
import {
motion,
useAnimate,
useMotionValue,
useTransform,
easeOut,
easeInOut,
} from "motion/react";
import { useEffect, useState, useRef } from "react";
import {
backgroundVariants,
DURATION,
handVariants,
IDLE_DURATION,
lineVariants,
defaultStrokeDashoffsets,
REPEAT_DELAY,
} from "./variants";
import { useAnimateVariant } from "./use-animate-variant";
const handPaths = [
"M58.5431 71.4419C60.2401 71.6239 61.3091 69.6649 60.2391 68.3369L49.2671 54.7189C48.6804 53.9554 48.4158 52.9927 48.5299 52.0366C48.6439 51.0805 49.1275 50.207 49.8773 49.6028C50.6271 48.9987 51.5835 48.712 52.542 48.804C53.5004 48.8959 54.3849 49.3593 55.0061 50.0949L58.3501 54.2449C58.8429 54.8569 59.5061 55.3089 60.2559 55.5437C61.0057 55.7785 61.8083 55.7855 62.5621 55.5639L70.6681 53.1809C72.8705 52.5335 75.23 52.6775 77.3372 53.5881C79.4444 54.4987 81.1663 56.1184 82.2041 58.1659L84.9681 63.6179C85.7915 65.2412 86.0321 67.098 85.6498 68.8775C85.2675 70.657 84.2855 72.2511 82.8681 73.3929L74.6611 80.0039C73.7139 80.7673 72.604 81.3028 71.417 81.5691C70.23 81.8355 68.9976 81.8255 67.8151 81.5399L55.1911 78.4919C54.2783 78.2707 53.4839 77.7106 52.9688 76.9252C52.4537 76.1399 52.2566 75.188 52.4174 74.2627C52.5781 73.3374 53.0848 72.5078 53.8346 71.9422C54.5844 71.3766 55.5212 71.1173 56.4551 71.2169L58.5431 71.4419Z",
"M58.7957 70.4997C60.4927 70.6817 61.3092 69.6653 60.2392 68.3373L51.8366 57.8368C51.2499 57.0733 50.867 55.9897 50.981 55.0336C51.0951 54.0776 51.5787 53.204 52.3285 52.5999C53.0783 51.9958 54.0347 51.7091 54.9931 51.801C55.9516 51.893 56.7918 52.4457 57.413 53.1813L58.3502 54.2453C58.8429 54.8573 59.5062 55.3093 60.256 55.5441C61.0058 55.7789 61.8084 55.7859 62.5622 55.5643L70.6681 53.1813C72.8705 52.5339 75.23 52.6779 77.3373 53.5885C79.4445 54.4991 81.1664 56.1187 82.2042 58.1663L84.9682 63.6183C85.7915 65.2416 86.0322 67.0984 85.6499 68.8778C85.2676 70.6573 84.2855 72.2515 82.8682 73.3933L74.6611 80.0043C73.714 80.7677 72.604 81.3032 71.417 81.5695C70.23 81.8358 68.9977 81.8258 67.8151 81.5403L55.5115 77.3318C54.5988 77.1105 53.8043 76.5504 53.2892 75.765C52.7742 74.9797 52.577 74.0279 52.7378 73.1025C52.8986 72.1772 53.4052 71.3476 54.155 70.782C54.9048 70.2165 55.8416 69.9572 56.7755 70.0567L58.7957 70.4997Z"
];
export default function ClickAnimation() {
const [scope, animateVariant, animate] = useAnimateVariant();
const handPathProgress = useMotionValue(0);
const handPath = useTransform(handPathProgress, [0, 1], handPaths);
const handPathAnimationRef = useRef(null);
const hasAnimationCompletedRef = useRef(false);
// Consolidated animation orchestration
const playAnimationState = async (
// "initial" | "hover" | "idle" | "click"
variant,
// optional path morphing config: { keyframes: number[]; times: number[]; repeat?: boolean; }
pathConfig
) => {
// Stop any in-flight animations
handPathAnimationRef.current?.stop();
if (!pathConfig?.keyframes) {
await animate(handPathProgress, 0);
}
const animations = [];
// Background
animations.push(
animateVariant('[data-animate="background"]', backgroundVariants[variant])
);
// Hand (no idle variant)
if (variant !== "idle") {
animations.push(
animateVariant('[data-animate="hand"]', handVariants[variant])
);
}
// Lines (indexed)
animations.push(
...Array.from({ length: 3 }, (_, i) => {
return animateVariant(
`[data-animate="line"][data-index='${i}']`,
lineVariants[variant](i),
);
})
);
// Animate path morphing if config provided
if (pathConfig) {
const pathAnimation = animate(handPathProgress, pathConfig.keyframes, {
duration: pathConfig.repeat ? IDLE_DURATION : DURATION,
times: pathConfig.times,
ease: easeOut,
...(pathConfig.repeat && {
repeat: Infinity,
repeatType: "loop",
repeatDelay: REPEAT_DELAY,
delay: REPEAT_DELAY,
}),
});
handPathAnimationRef.current = pathAnimation;
}
return Promise.all(animations);
};
const startIdleAnimations = async () => {
await playAnimationState("idle", {
keyframes: [0, 1, 0],
times: [0.1, 0.4, 0.8],
repeat: true,
});
};
useEffect(() => {
startIdleAnimations();
return () => {
handPathAnimationRef.current?.stop();
};
}, [startIdleAnimations]);
const handleMouseEnter = async () => {
await playAnimationState("hover", {
keyframes: [0, 1, 0],
times: [0.4, 0.6, 0.9],
});
hasAnimationCompletedRef.current = true;
};
const handleMouseLeave = async () => {
hasAnimationCompletedRef.current = false;
await playAnimationState("initial");
startIdleAnimations();
};
const handleClick = () => {
if (!hasAnimationCompletedRef.current) return;
playAnimationState("click", {
keyframes: [0, 1, 0],
times: [0.1, 0.3, 0.6],
});
};
return (
);
}
```
We’ve covered a lot - coordinated timing, path morphing, stroke animations, and organizing it all with variants. The patterns you’ve learned here will serve you well as you build more complex animations.
Try experimenting with the timing values or adding your own touches. Small adjustments can completely change the feel of an animation.
In the next lesson, we’ll apply these techniques to build the clock animation with an interesting easter egg. You’ll see how the same coordination patterns work for different types of motion.
---
# Clock animation
- Module: Hero Illustration
- Lesson URL: /lessons/hero-illustration/clock-animation/
- Markdown URL: /md/lessons/hero-illustration/clock-animation.md
- Source mirror path: apps/anim/learn/hero-illustration/clock-animation.html
This clock has a fun easter egg - click it and the hands will spin to show you the actual time. We’ll build this animation step by step: bells swinging sideways in idle state, alarm going off on hover, current time animation on click and feedback on subsequent clicks.
## Overview
This clock has a fun easter egg - click it and the hands will spin to show you the actual time. We’ll build this animation step by step: bells swinging sideways in idle state, alarm going off on hover, current time animation on click and feedback on subsequent clicks.
Let’s start with the SVG structure.
## The SVG structure
The SVG has three main elements:
- Background: This will scale and rotate based on state.
- Clock: Containing the clock face and the hour and minute hands.
- Bells: Two bells on top of the clock.
```jsx
```
```jsx
```
The bells are grouped with the clock inside clock-and-bells so we can rotate and scale them together during certain animations while still animating them independently during others. Both bells can also be animated individually using their data-index attributes.
## Idle: The bells swing
Let’s start by creating the idle animation for the bells. We want to swing the bells group from side to side. If you try to do that with transformBox: fill-box it becomes tricky - rotating the bells around their own center just tilts them, so you’d need to add translateX and translateY to balance the movement.
A simpler solution is to rotate the bells around the clock center point. This way we can just use rotate without complicating the transform:
```jsx
{/* bell elements */}
```
```jsx
{/* bell elements */}
```
We use explicit pixel coordinates for transformOrigin. The transformBox: "view-box" style makes these coordinates relative to the SVG’s viewBox.
Notice we set transformOrigin in initial rather than style. This is important because when you set it in style, Motion will override it with its default "50% 50%" origin for SVG elements.
Now the swing animation:
```jsx
const bellsVariants = {
initial: {
rotate: 0,
transformOrigin: CLOCK_CENTER,
},
idle: {
rotate: [0, -10, 10, 0],
transition: {
duration: 1,
ease: easeInOut,
repeat: Infinity,
repeatType: "loop",
repeatDelay: 2,
delay: 2,
},
},
};
```
```jsx
const bellsVariants = {
initial: {
rotate: 0,
transformOrigin: CLOCK_CENTER,
},
idle: {
rotate: [0, -10, 10, 0],
transition: {
duration: 1,
ease: easeInOut,
repeat: Infinity,
repeatType: "loop",
repeatDelay: 2,
delay: 2,
},
},
};
```
The bells rotate left, then right, then back to center, repeating every 2 seconds. We can use animateVariant directly to start this animation on mount since it’s the only idle animation in this component:
```jsx
const [scope, animateVariant] = useAnimateVariant();
useEffect(() => {
animateVariant('[data-animate="bells"]', bellsVariants.idle);
}, []);
```
```jsx
const [scope, animateVariant] = useAnimateVariant();
useEffect(() => {
animateVariant('[data-animate="bells"]', bellsVariants.idle);
}, []);
```
```text
"use client";
import { useRef, useEffect } from "react";
import {
motion,
useAnimate,
useMotionValue,
useTransform,
easeOut,
easeInOut,
} from "motion/react";
import { useAnimateVariant } from "./use-animate-variant";
import { bellsVariants } from "./clock-variants";
const INITIAL_HOUR_ROTATION = 120;
export default function ClockAnimation() {
const [scope, animateVariant, animate] = useAnimateVariant();
useEffect(() => {
animateVariant('[data-animate="bells"]', bellsVariants.idle);
}, [animateVariant]);
return (
);
}
```
## Hover: The alarm goes off
When you hover, the background tilts and compresses, the clock shakes horizontally, the bells group moves up, and each individual bell shakes independently.
The background uses a simple rotate and scale sequence:
```jsx
const backgroundVariants = {
initial: {
transform: "rotate(0deg) scale(1)",
},
hover: {
transform: [
"rotate(0deg) scale(1)",
"rotate(-4deg) scale(0.99)",
"rotate(-3deg) scale(1)",
],
transition: {
duration: 0.3,
ease: easeOut,
},
},
};
```
```jsx
const backgroundVariants = {
initial: {
transform: "rotate(0deg) scale(1)",
},
hover: {
transform: [
"rotate(0deg) scale(1)",
"rotate(-4deg) scale(0.99)",
"rotate(-3deg) scale(1)",
],
transition: {
duration: 0.3,
ease: easeOut,
},
},
};
```
The clock-and-bells group translates up to create a jump effect:
```jsx
const clockAndBellsVariants = {
initial: {
transform: "translateY(0px) rotate(0deg) scale(1)",
},
hover: {
transform: [
"translateY(0px) rotate(0deg) scale(1)",
"translateY(-3px) rotate(0deg) scale(1)",
],
transition: {
duration: 0.3,
ease: easeOut,
},
},
};
```
```jsx
const clockAndBellsVariants = {
initial: {
transform: "translateY(0px) rotate(0deg) scale(1)",
},
hover: {
transform: [
"translateY(0px) rotate(0deg) scale(1)",
"translateY(-3px) rotate(0deg) scale(1)",
],
transition: {
duration: 0.3,
ease: easeOut,
},
},
};
```
The clock face (including the clock hands) shakes horizontally with a continuous x-axis oscillation:
```jsx
const clockVariants = {
initial: { x: "0px" },
hover: {
x: ["0px", "-1.5px", "1.75px", "-1.75px", "1.75px", "-1.5px", "0px"],
transition: {
x: {
duration: 0.25,
repeat: Infinity,
ease: linear,
},
},
},
};
```
```jsx
const clockVariants = {
initial: { x: "0px" },
hover: {
x: ["0px", "-1.5px", "1.75px", "-1.75px", "1.75px", "-1.5px", "0px"],
transition: {
x: {
duration: 0.25,
repeat: Infinity,
ease: linear,
},
},
},
};
```
The bells group animates up slowly over 3 seconds:
```jsx
const bellsVariants = {
// ...initial and idle
hover: {
y: [0, -8],
rotate: 0,
transition: {
y: {
duration: 3,
ease: easeOut,
},
},
},
};
```
```jsx
const bellsVariants = {
// ...initial and idle
hover: {
y: [0, -8],
rotate: 0,
transition: {
y: {
duration: 3,
ease: easeOut,
},
},
},
};
```
Each bell shakes independently. The second bell uses a smaller oscillation since it’s smaller, and rotates as the bells group moves up to stay facing the clock:
```jsx
const bellVariants = {
initial: {
x: "0px",
rotate: "0deg",
},
hover: (i: number) => ({
x:
i === 0
? ["0px", "-2px", "2px", "-2px", "2px", "-2px", "0%"]
: ["0px", "1.5px", "-1.5px", "1.5px", "-1.5px", "1.5px", "0%"],
rotate: i === 0 ? "0deg" : ["0deg", "-8deg"],
transition: {
x: {
duration: 0.25,
repeat: Infinity,
ease: linear,
},
rotate: {
duration: 3,
ease: easeOut,
},
},
}),
};
```
```jsx
const bellVariants = {
initial: {
x: "0px",
rotate: "0deg",
},
hover: (i: number) => ({
x:
i === 0
? ["0px", "-2px", "2px", "-2px", "2px", "-2px", "0%"]
: ["0px", "1.5px", "-1.5px", "1.5px", "-1.5px", "1.5px", "0%"],
rotate: i === 0 ? "0deg" : ["0deg", "-8deg"],
transition: {
x: {
duration: 0.25,
repeat: Infinity,
ease: linear,
},
rotate: {
duration: 3,
ease: easeOut,
},
},
}),
};
```
To animate these, we need an orchestrator that can target specific elements and coordinate all the animations:
```jsx
const animateClockVariant = (variant) => {
const animations = [
animateVariant("[data-animate='clock']", clockVariants[variant]),
animateVariant(
"[data-animate='clock-and-bells']",
clockAndBellsVariants[variant]
),
animateVariant('[data-animate="bells"]', bellsVariants[variant]),
...Array.from({ length: 2 }, (_, i) =>
animateVariant(
`[data-animate="bell"][data-index='${i}']`,
typeof bellVariants[variant] === "function"
? bellVariants[variant](i)
: bellVariants[variant]
)
),
animateVariant('[data-animate="background"]', backgroundVariants[variant]),
].filter(Boolean);
return Promise.all(animations);
};
const handleMouseEnter = () => {
animateClockVariant("hover");
};
const handleMouseLeave = () => {
animateClockVariant("initial");
};
```
```jsx
const animateClockVariant = (variant) => {
const animations = [
animateVariant("[data-animate='clock']", clockVariants[variant]),
animateVariant(
"[data-animate='clock-and-bells']",
clockAndBellsVariants[variant]
),
animateVariant('[data-animate="bells"]', bellsVariants[variant]),
...Array.from({ length: 2 }, (_, i) =>
animateVariant(
`[data-animate="bell"][data-index='${i}']`,
typeof bellVariants[variant] === "function"
? bellVariants[variant](i)
: bellVariants[variant]
)
),
animateVariant('[data-animate="background"]', backgroundVariants[variant]),
].filter(Boolean);
return Promise.all(animations);
};
const handleMouseEnter = () => {
animateClockVariant("hover");
};
const handleMouseLeave = () => {
animateClockVariant("initial");
};
```
```text
"use client";
import { useRef, useEffect } from "react";
import {
motion,
useAnimate,
useMotionValue,
useTransform,
easeOut,
easeInOut,
} from "motion/react";
import { useAnimateVariant } from "./use-animate-variant";
import { bellsVariants, backgroundVariants, clockVariants, clockAndBellsVariants, bellVariants } from "./clock-variants";
const INITIAL_HOUR_ROTATION = 120;
export default function ClockAnimation() {
const [scope, animateVariant, animate] = useAnimateVariant();
useEffect(() => {
animateVariant('[data-animate="bells"]', bellsVariants.idle);
}, [animateVariant]);
const animateClockVariant = (variant) => {
const animations = [
animateVariant("[data-animate='clock']", clockVariants[variant]),
animateVariant("[data-animate='clock-and-bells']", clockAndBellsVariants[variant]),
animateVariant('[data-animate="bells"]', bellsVariants[variant]),
...Array.from({ length: 2 }, (_, i) =>
animateVariant(
`[data-animate="bell"][data-index='${i}']`,
typeof bellVariants[variant] === "function"
? bellVariants[variant](i)
: bellVariants[variant]
)
),
animateVariant('[data-animate="background"]', backgroundVariants[variant]),
].filter(Boolean);
return Promise.all(animations);
}
const handleMouseEnter = () => {
animateClockVariant("hover");
}
const handleMouseLeave = () => {
animateClockVariant("initial");
}
return (
);
}
```
## Click: Show the time
On the first click, the bells stop ringing and the clock hands spin to show the current time.
The clock hands start pointing at 12:00 in their SVG coordinates. We rotate the hour hand to 120 degrees for the default display position.
After clicking, the clock hands need to rotate around their base (the clock center), similar to how the bells rotate around the clock center, but here we use transformBox: "fill-box" since they are already positioned in the center of the clock face.
We set this up with transform origins using Motion’s initial prop:
```jsx
```
```jsx
```
The transformOrigin: "0% 100%" sets the rotation point to the bottom-left corner of the line’s bounding box, which is where the hand connects to the clock center. Motion automatically applies transformBox: "fill-box" to make those percentages relative to the element’s shape.
Now we can calculate where the hands should point:
```jsx
const now = new Date();
const hours = now.getHours() % 12;
const minutes = now.getMinutes();
const seconds = now.getSeconds();
// 30 degrees per hour (360° / 12 hours)
const hourRotation = hours * 30 + minutes * 0.5;
// 6 degrees per minute (360° / 60 minutes)
const minuteRotation = minutes * 6 + seconds * 0.1;
```
```jsx
const now = new Date();
const hours = now.getHours() % 12;
const minutes = now.getMinutes();
const seconds = now.getSeconds();
// 30 degrees per hour (360° / 12 hours)
const hourRotation = hours * 30 + minutes * 0.5;
// 6 degrees per minute (360° / 60 minutes)
const minuteRotation = minutes * 6 + seconds * 0.1;
```
The clock face has 360 degrees. Divide by 12 hours gives us 30 degrees per hour. Same logic for minutes.
To make the hands spin before landing, we add full rotations:
```jsx
const [scope, animateVariant, animate] = useAnimateVariant();
const SPRING_CONFIG = {
type: "spring",
stiffness: 250,
damping: 25,
mass: 1.2,
};
const animateClockHands = (hourRotation, minuteRotation) => {
animate(
"[data-animate='hour-hand']",
{ transform: `rotate(${hourRotation}deg)` },
SPRING_CONFIG
);
animate(
"[data-animate='minute-hand']",
{ transform: `rotate(${minuteRotation}deg)` },
SPRING_CONFIG
);
};
const hourSpins = 1;
const minuteSpins = 2;
animateClockHands(
360 * hourSpins + hourRotation,
360 * minuteSpins + minuteRotation
);
```
```jsx
const [scope, animateVariant, animate] = useAnimateVariant();
const SPRING_CONFIG = {
type: "spring",
stiffness: 250,
damping: 25,
mass: 1.2,
};
const animateClockHands = (hourRotation, minuteRotation) => {
animate(
"[data-animate='hour-hand']",
{ transform: `rotate(${hourRotation}deg)` },
SPRING_CONFIG
);
animate(
"[data-animate='minute-hand']",
{ transform: `rotate(${minuteRotation}deg)` },
SPRING_CONFIG
);
};
const hourSpins = 1;
const minuteSpins = 2;
animateClockHands(
360 * hourSpins + hourRotation,
360 * minuteSpins + minuteRotation
);
```
The hour hand does one full spin, the minute hand does two. Spring transitions create a natural, bouncy spin effect.
While the hands spin, the clock-and-bells group animates into an upright position to create a settling effect:
```jsx
const clockAndBellsVariants = {
// ...initial and hover
click: {
transform: [
"translateY(0px) rotate(0deg) scale(1)",
"translateY(0px) rotate(-10deg) scale(0.95)",
"translateY(0px) rotate(-8deg) scale(1.03)",
"translateY(0px) rotate(-8deg) scale(1)",
],
transition: {
duration: 0.4,
times: [0, 0.25, 0.6, 1],
ease: easeOut,
},
},
};
```
```jsx
const clockAndBellsVariants = {
// ...initial and hover
click: {
transform: [
"translateY(0px) rotate(0deg) scale(1)",
"translateY(0px) rotate(-10deg) scale(0.95)",
"translateY(0px) rotate(-8deg) scale(1.03)",
"translateY(0px) rotate(-8deg) scale(1)",
],
transition: {
duration: 0.4,
times: [0, 0.25, 0.6, 1],
ease: easeOut,
},
},
};
```
The click handler brings everything together:
```jsx
const handleClick = () => {
animateClockVariant("click");
const now = new Date();
const hours = now.getHours() % 12;
const minutes = now.getMinutes();
const seconds = now.getSeconds();
const newHourRotation = hours * 30 + minutes * 0.5;
const newMinuteRotation = minutes * 6 + seconds * 0.1;
const hourWithSpins = 360 * 1 + newHourRotation;
const minuteWithSpins = 360 * 2 + newMinuteRotation;
animateClockHands(hourWithSpins, minuteWithSpins);
};
```
```jsx
const handleClick = () => {
animateClockVariant("click");
const now = new Date();
const hours = now.getHours() % 12;
const minutes = now.getMinutes();
const seconds = now.getSeconds();
const newHourRotation = hours * 30 + minutes * 0.5;
const newMinuteRotation = minutes * 6 + seconds * 0.1;
const hourWithSpins = 360 * 1 + newHourRotation;
const minuteWithSpins = 360 * 2 + newMinuteRotation;
animateClockHands(hourWithSpins, minuteWithSpins);
};
```
On mouse leave, we reset everything back to the initial state and restart the idle animation:
```jsx
const handleMouseLeave = async () => {
animateClockHands(INITIAL_HOUR_ROTATION, 0);
await animateClockVariant("initial");
animateVariant('[data-animate="bells"]', bellsVariants.idle);
};
```
```jsx
const handleMouseLeave = async () => {
animateClockHands(INITIAL_HOUR_ROTATION, 0);
await animateClockVariant("initial");
animateVariant('[data-animate="bells"]', bellsVariants.idle);
};
```
```text
"use client";
import { useRef, useEffect } from "react";
import {
motion,
useAnimate,
useMotionValue,
useTransform,
easeOut,
easeInOut,
} from "motion/react";
import { useAnimateVariant } from "./use-animate-variant";
import { bellsVariants, backgroundVariants, clockVariants, clockAndBellsVariants, bellVariants } from "./clock-variants";
const INITIAL_HOUR_ROTATION = 120;
const SPRING_CONFIG = {
type: "spring",
stiffness: 250,
damping: 25,
mass: 1.2,
}
export default function ClockAnimation() {
const [scope, animateVariant, animate] = useAnimateVariant();
useEffect(() => {
animateVariant('[data-animate="bells"]', bellsVariants.idle);
}, [animateVariant]);
const animateClockVariant = (variant) => {
const animations = [
animateVariant("[data-animate='clock']", clockVariants[variant] ?? clockVariants.initial),
animateVariant("[data-animate='clock-and-bells']", clockAndBellsVariants[variant] ?? clockAndBellsVariants.initial),
animateVariant('[data-animate="bells"]', bellsVariants[variant] ?? bellsVariants.initial),
...Array.from({ length: 2 }, (_, i) =>
animateVariant(
`[data-animate="bell"][data-index='${i}']`,
typeof bellVariants[variant] === "function"
? bellVariants[variant](i)
: bellVariants[variant] ?? bellVariants.initial
)
),
animateVariant('[data-animate="background"]', backgroundVariants[variant] ?? backgroundVariants.initial),
].filter(Boolean);
return Promise.all(animations);
}
const animateClockHands = (hourRotation, minuteRotation) => {
animate(
"[data-animate='hour-hand']",
{
transform: `rotate(${hourRotation}deg)`,
},
SPRING_CONFIG
);
animate(
"[data-animate='minute-hand']",
{
transform: `rotate(${minuteRotation}deg)`,
},
SPRING_CONFIG
);
}
const handleMouseEnter = () => {
animateClockVariant("hover");
}
const handleMouseLeave = async () => {
animateClockHands(INITIAL_HOUR_ROTATION, 0);
await animateClockVariant("initial");
animateVariant('[data-animate="bells"]', bellsVariants.idle);
}
const handleClick = () => {
animateVariant("[data-animate='background']", backgroundVariants.click);
animateClockVariant("click");
const now = new Date();
const hours = now.getHours() % 12;
const minutes = now.getMinutes();
const seconds = now.getSeconds();
// Calculate rotations (0 degrees is at 12 o'clock position)
// Hour hand: moves 30 degrees per hour (360/12) plus 0.5 degrees per minute
const newHourRotation = hours * 30 + minutes * 0.5;
// Minute hand: moves 6 degrees per minute (360/60)
const newMinuteRotation = minutes * 6 + seconds * 0.1;
const hourSpins = 1;
const minuteSpins = 2;
const hourWithSpins = 360 * hourSpins + newHourRotation;
const minuteWithSpins = 360 * minuteSpins + newMinuteRotation;
animateClockHands(hourWithSpins, minuteWithSpins);
}
return (
);
}
```
## Click again: Just a pulse
After the first click, the clock settles into its upright position. Clicking again would replay the same animation, tilting it back upright - we don’t want that.
On subsequent clicks, we can ignore the hands as they’re already showing the time. We just want feedback to acknowledge the click:
```jsx
const clockAndBellsVariants = {
// ...other variants
"scale-click": {
transform: [
"translateY(0px) rotate(-8deg) scale(1)",
"translateY(0px) rotate(-8deg) scale(0.95)",
"translateY(0px) rotate(-8deg) scale(1.03)",
"translateY(0px) rotate(-8deg) scale(1)",
],
transition: {
duration: 0.4,
times: [0, 0.25, 0.6, 1],
ease: "easeOut",
},
},
};
```
```jsx
const clockAndBellsVariants = {
// ...other variants
"scale-click": {
transform: [
"translateY(0px) rotate(-8deg) scale(1)",
"translateY(0px) rotate(-8deg) scale(0.95)",
"translateY(0px) rotate(-8deg) scale(1.03)",
"translateY(0px) rotate(-8deg) scale(1)",
],
transition: {
duration: 0.4,
times: [0, 0.25, 0.6, 1],
ease: "easeOut",
},
},
};
```
This variant maintains the rotation from the first click but just animates the scale. The handler uses a ref to track whether this is the first click:
```jsx
const hasClickedRef = useRef(false);
const handleClick = () => {
if (!hasClickedRef.current) {
hasClickedRef.current = true;
// First click: animate to show time...
animateClockVariant("click");
// Calculate time and spin hands...
} else {
// Subsequent clicks: just pulse
animateClockVariant("scale-click");
}
};
```
```jsx
const hasClickedRef = useRef(false);
const handleClick = () => {
if (!hasClickedRef.current) {
hasClickedRef.current = true;
// First click: animate to show time...
animateClockVariant("click");
// Calculate time and spin hands...
} else {
// Subsequent clicks: just pulse
animateClockVariant("scale-click");
}
};
```
When the user leaves the clock, we reset the ref so it starts fresh next time:
```jsx
const handleMouseLeave = async () => {
hasClickedRef.current = false;
animateClockHands(INITIAL_HOUR_ROTATION, 0);
await animateClockVariant("initial");
animateVariant('[data-animate="bells"]', bellsVariants.idle);
};
```
```jsx
const handleMouseLeave = async () => {
hasClickedRef.current = false;
animateClockHands(INITIAL_HOUR_ROTATION, 0);
await animateClockVariant("initial");
animateVariant('[data-animate="bells"]', bellsVariants.idle);
};
```
```text
"use client";
import { useRef, useEffect } from "react";
import {
motion,
useAnimate,
useMotionValue,
useTransform,
easeOut,
easeInOut,
} from "motion/react";
import { useAnimateVariant } from "./use-animate-variant";
import { bellsVariants, backgroundVariants, clockVariants, clockAndBellsVariants, bellVariants } from "./clock-variants";
const INITIAL_HOUR_ROTATION = 120;
const SPRING_CONFIG = {
type: "spring",
stiffness: 250,
damping: 25,
mass: 1.2,
}
export default function ClockAnimation() {
const [scope, animateVariant, animate] = useAnimateVariant();
const hasClickedRef = useRef(false);
useEffect(() => {
animateVariant('[data-animate="bells"]', bellsVariants.idle);
}, [animateVariant]);
const animateClockVariant = (variant) => {
const animations = [
animateVariant("[data-animate='clock']", clockVariants[variant] ?? clockVariants.initial),
animateVariant("[data-animate='clock-and-bells']", clockAndBellsVariants[variant] ?? clockAndBellsVariants.initial),
animateVariant('[data-animate="bells"]', bellsVariants[variant] ?? bellsVariants.initial),
...Array.from({ length: 2 }, (_, i) =>
animateVariant(
`[data-animate="bell"][data-index='${i}']`,
typeof bellVariants[variant] === "function"
? bellVariants[variant](i)
: bellVariants[variant] ?? bellVariants.initial
)
),
animateVariant('[data-animate="background"]', backgroundVariants[variant] ?? backgroundVariants.initial),
].filter(Boolean);
return Promise.all(animations);
}
const animateClockHands = (hourRotation, minuteRotation) => {
animate(
"[data-animate='hour-hand']",
{
transform: `rotate(${hourRotation}deg)`,
},
SPRING_CONFIG
);
animate(
"[data-animate='minute-hand']",
{
transform: `rotate(${minuteRotation}deg)`,
},
SPRING_CONFIG
);
}
const handleMouseEnter = () => {
animateClockVariant("hover");
}
const handleMouseLeave = async () => {
hasClickedRef.current = false;
animateClockHands(INITIAL_HOUR_ROTATION, 0);
await animateClockVariant("initial");
animateVariant('[data-animate="bells"]', bellsVariants.idle);
}
const handleClick = () => {
if (!hasClickedRef.current) {
hasClickedRef.current = true;
animateClockVariant("click");
const now = new Date();
const hours = now.getHours() % 12;
const minutes = now.getMinutes();
const seconds = now.getSeconds();
// Calculate rotations (0 degrees is at 12 o'clock position)
// Hour hand: moves 30 degrees per hour (360/12) plus 0.5 degrees per minute
const newHourRotation = hours * 30 + minutes * 0.5;
// Minute hand: moves 6 degrees per minute (360/60)
const newMinuteRotation = minutes * 6 + seconds * 0.1;
const hourSpins = 1;
const minuteSpins = 2;
const hourWithSpins = 360 * hourSpins + newHourRotation;
const minuteWithSpins = 360 * minuteSpins + newMinuteRotation;
animateClockHands(hourWithSpins, minuteWithSpins);
} else {
animateClockVariant("scale-click");
}
}
return (
);
}
```
In the next lesson, we’ll polish this into a production-ready component with mobile support, accessibility features, and performance optimizations.
---
# Lines and dashes
- Module: Hero Illustration
- Lesson URL: /lessons/hero-illustration/lines-and-dashes/
- Markdown URL: /md/lessons/hero-illustration/lines-and-dashes.md
- Source mirror path: apps/anim/learn/hero-illustration/lines-and-dashes.html
You’ve probably seen this effect before, a path that looks like it’s drawing itself. Loading spinners, signature animations, icon reveals. It’s everywhere. In this case, it’s the hand’s rays.
## Overview
You’ve probably seen this effect before, a path that looks like it’s drawing itself. Loading spinners, signature animations, icon reveals. It’s everywhere. In this case, it’s the hand’s rays.
The technique is surprisingly simple once you know the trick.
## Dash patterns
SVG paths can have dashed strokes using strokeDasharray. This property takes two values: the dash length and the gap length.
```tsx
// 20 units dash, 10 units gap
```
```tsx
// 20 units dash, 10 units gap
```
Notice how in the demo below, the pattern automatically repeats along the entire path. This works on any SVG element with a stroke - lines, circles, rectangles, complex paths - the pattern tiles across its full length.
```text
```
## Shifting with dash offset
Now that we have a dash pattern, we can shift it along the path using strokeDashoffset. Positive values shift the pattern backward, negative values shift it forward.
```tsx
// Shift pattern 30 units backward
```
```tsx
// Shift pattern 30 units backward
```
This shifting is what makes drawing animations possible - by animating the offset, we can control which part of the pattern is visible.
```text
```
## Bringing it together
By combining these two properties in a specific way, we can create the line drawing effect:
- Set the dash length to the path’s full length (one continuous dash)
- Set a large gap so only one dash is visible
- Offset the dash by the path length to hide it completely
- Animate the offset back to 0 to reveal the line
Here’s what that looks like in action:
```text
```
The path starts hidden because the dash is offset by its full length. As strokeDashoffset animates from 100 to 0, the line appears to draw itself.
This is the exact technique we’ll use to animate the hand rays in the next lesson.
## A note on pathLength
In the animation above, the path uses pathLength="100". This property overrides the path’s actual pixel length - the browser uses your custom value for all stroke calculations.
This is useful for normalizing paths to round numbers. Set it to 100 to work in percentages, or use the same value across multiple paths so they can share animation values regardless of their actual sizes.
## Exercise: Animate a checkbox
Let’s practice the line drawing technique. Below is a checkbox with a square and checkmark - both have pathLength="100" already set.
Your task: Make both elements draw themselves when you click the button, with the square completing before the checkmark starts.
Success criteria:
- Both shapes start hidden
- Square draws itself first
- Checkmark draws after the square finishes
```text
import "./styles.css";
import { useState } from "react";
export default function CheckboxAnimation() {
const [reset, setReset] = useState(0);
return (
);
}
```
---
# Polish
- Module: Hero Illustration
- Lesson URL: /lessons/hero-illustration/polish/
- Markdown URL: /md/lessons/hero-illustration/polish.md
- Source mirror path: apps/anim/learn/hero-illustration/polish.html
The clock works. It responds to hover, animates the hands to the current time, handles all the interactions. But something’s missing. That subtle aliveness you see in UIs that truly stand out. Let’s improve it!
## Overview
The clock works. It responds to hover, animates the hands to the current time, handles all the interactions. But something’s missing. That subtle aliveness you see in UIs that truly stand out. Let’s improve it!
We’ll work on subtle movement, better interaction feel, mobile support, accessibility, and performance optimization for complex scenes.
## Subtle life
When idle, everything is completely still. It works, but it doesn’t feel alive. We need some barely-noticeable movement to make the scene feel more organic.
We’ll add gentle floating to the illustrations and subtle rotation to the backgrounds. Instead of writing animation logic in useEffect hooks, we’ll create helper functions that return animation props we can spread directly onto motion.g elements.
```jsx
export const createFloatingAnimation = ({ to = 2, duration = 2.5 }) => {
return {
initial: { transform: "translateY(0px)" },
animate: {
transform: ["translateY(0px)", `translateY(${to}px)`],
},
transition: {
duration,
ease: "easeInOut",
repeat: Infinity,
repeatType: "reverse",
},
};
};
export const createRotationAnimation = ({ to = 2, duration = 5 }) => {
return {
initial: { transform: "rotate(0deg)" },
animate: {
transform: ["rotate(0deg)", `rotate(${to}deg)`],
},
transition: {
duration,
ease: "easeInOut",
repeat: Infinity,
repeatType: "reverse",
},
};
};
```
```jsx
export const createFloatingAnimation = ({ to = 2, duration = 2.5 }) => {
return {
initial: { transform: "translateY(0px)" },
animate: {
transform: ["translateY(0px)", `translateY(${to}px)`],
},
transition: {
duration,
ease: "easeInOut",
repeat: Infinity,
repeatType: "reverse",
},
};
};
export const createRotationAnimation = ({ to = 2, duration = 5 }) => {
return {
initial: { transform: "rotate(0deg)" },
animate: {
transform: ["rotate(0deg)", `rotate(${to}deg)`],
},
transition: {
duration,
ease: "easeInOut",
repeat: Infinity,
repeatType: "reverse",
},
};
};
```
Now we can apply them directly:
```jsx
{/* background path */}
{/* other elements */}
```
```jsx
{/* background path */}
{/* other elements */}
```
The outer group floats the entire illustration with translateY(1.5px) over 3 seconds. The inner group rotates just the background with rotate(2deg) over 4 seconds. Different timings mean they never sync up. That’s why it feels organic and not mechanical.
Grid lines were added to emphasize the subtle movement.
It’s very subtle, but once all elements in the scene are animated in this subtle way, it creates a sense of life and movement. Especially when all the elements have some special, unique movement to them too from time to time, like the lightbulb blinking.
## Preventing accidental triggers
Try moving your mouse quickly across the clock below. The animation triggers way too quickly, it’s jittery and annoying. That’s why tooltips have slight delays to them for example, to prevent accidental triggers.
The fix is to simply trigger the hover only if the mouse stays for a tiny bit.
There are a few ways to do this, but a simple approach is debouncing the hover with a useHoverTimeout hook. This ensures that the hover will be triggered only after you hovered for at least 100ms. You can see the full implementation of the hook here.
It accepts a delay prop, which in our case is 100ms and two callbacks, onHoverStart and onHoverEnd that fire when the hover starts (after the delay) and ends. We can then use this hook and apply it to each of the interactions.
```jsx
const { handleMouseEnter, handleMouseLeave } = useHoverTimeout({
delay: 100,
onHoverStart: () => {
// play hover animations
},
onHoverEnd: () => {
// return to idle animations
},
});
return (
{/* illustration */}
);
```
```jsx
const { handleMouseEnter, handleMouseLeave } = useHoverTimeout({
delay: 100,
onHoverStart: () => {
// play hover animations
},
onHoverEnd: () => {
// return to idle animations
},
});
return (
{/* illustration */}
);
```
100ms is short enough to feel responsive, long enough to prevent accidental triggers. Try rapid mouse movements below and see how it feels.
## Mobile support
Our clock animation works great on dekstop, but on mobile, there’s a problem. Hover and click handlers fire at once on touch devices. The demo below emulates the behavior a mobile user will experience with our current implementation. Try interacting with it.
Emulation of mobile interaction without handling hover state
There’s no hover feedback as there is no hover on mobile. And a single click triggers both the hover and click animations immediately – you can even see the background glitch slightly as it quickly transitions from the hover to the click animation. There’s also no mouse leave event – the hover state only resets when you tap outside the illustration.
To fix this, we will need a two-tap pattern on mobile devices: first tap to trigger the hover animation and second tap to trigger the click animation.
To build this, we need to detect touch devices and track tap state. Let’s start with a media query to detect touch devices:
```jsx
import { useMediaQuery } from "usehooks-ts";
// inside component
const isMobile = useMediaQuery("(pointer: coarse)");
```
```jsx
import { useMediaQuery } from "usehooks-ts";
// inside component
const isMobile = useMediaQuery("(pointer: coarse)");
```
The pointer: coarse media query detects if the primary input device has limited pointing accuracy – touch screens. A narrow desktop browser window still returns false because it has a precise pointer.
On mobile, we want: tap once to trigger the hover animation, and tap again to trigger the click animation. We can create a simple hook to help us implement this behaviour across many components:
```jsx
import { useMediaQuery } from "usehooks-ts";
import { useEffect, useRef, useCallback } from "react";
export function useMobileTap() {
const isMobile = useMediaQuery("(pointer: coarse)");
const isReadyForClickRef = useRef(true);
useEffect(() => {
if (isMobile) {
isReadyForClickRef.current = false;
}
}, [isMobile]);
const markTapped = useCallback(() => {
if (isMobile) {
isReadyForClickRef.current = true;
}
}, [isMobile]);
const resetTap = useCallback(() => {
if (isMobile) {
isReadyForClickRef.current = false;
}
}, [isMobile]);
return {
isReadyForClickRef,
markTapped,
resetTap,
};
}
```
```jsx
import { useMediaQuery } from "usehooks-ts";
import { useEffect, useRef, useCallback } from "react";
export function useMobileTap() {
const isMobile = useMediaQuery("(pointer: coarse)");
const isReadyForClickRef = useRef(true);
useEffect(() => {
if (isMobile) {
isReadyForClickRef.current = false;
}
}, [isMobile]);
const markTapped = useCallback(() => {
if (isMobile) {
isReadyForClickRef.current = true;
}
}, [isMobile]);
const resetTap = useCallback(() => {
if (isMobile) {
isReadyForClickRef.current = false;
}
}, [isMobile]);
return {
isReadyForClickRef,
markTapped,
resetTap,
};
}
```
On desktop, isReadyForClick is always true. On mobile, it starts as false. First tap calls markTapped() to enable the click, second tap proceeds with the click animation.
Now let’s connect this to our hover and click handlers. The trick is making handleClick return early on the first tap, which allows handleMouseEnter to fire and play the hover animation:
```jsx
const isMobile = useMediaQuery("(pointer: coarse)");
const { isReadyForClickRef, markTapped, resetTap } = useMobileTap();
const handleClick = () => {
if (!isReadyForClickRef.current) {
markTapped(); // First tap on mobile
return; // Return early to trigger the hover animation
}
// Only trigger click animation on second tap
};
const { handleMouseEnter, handleMouseLeave } = useHoverTimeout({
delay: isMobile ? 0 : 100, // No delay on mobile
onHoverStart: () => {
// Hover animation plays because of early return in handleClick
playWaveAnimation();
},
onHoverEnd: () => {
resetTap();
returnToIdle();
},
});
return (
{/* clock */}
);
```
```jsx
const isMobile = useMediaQuery("(pointer: coarse)");
const { isReadyForClickRef, markTapped, resetTap } = useMobileTap();
const handleClick = () => {
if (!isReadyForClickRef.current) {
markTapped(); // First tap on mobile
return; // Return early to trigger the hover animation
}
// Only trigger click animation on second tap
};
const { handleMouseEnter, handleMouseLeave } = useHoverTimeout({
delay: isMobile ? 0 : 100, // No delay on mobile
onHoverStart: () => {
// Hover animation plays because of early return in handleClick
playWaveAnimation();
},
onHoverEnd: () => {
resetTap();
returnToIdle();
},
});
return (
{/* clock */}
);
```
First tap: handleClick returns early, handleMouseEnter fires. Second tap: isReadyForClick is true, click animation runs. When the user taps outside, handleMouseLeave calls resetTap() to prepare for the next interaction.
We also set the delay to 0 in useHoverTimeout for mobile devices because the first tap should trigger the hover animation immediately as there’s no mouse movement to debounce.
Try it below. The first click triggers the hover animation, second one triggers the click animation and updates the time. Subsequent clicks give scale feedback.
Emulation of mobile interaction with hover state handled
## Respecting motion preferences
To prevent people from feeling sick or getting distracted, our animations need to be changed based on whether they have prefers-reduced-motion set in their OS. motion/react gives us a useReducedMotion hook to tell us exactly that.
```jsx
import { useReducedMotion } from "motion/react";
const shouldReduceMotion = useReducedMotion();
```
```jsx
import { useReducedMotion } from "motion/react";
const shouldReduceMotion = useReducedMotion();
```
When shouldReduceMotion is true, we disable all animations, floating, rotation, hover and click interactions. The asset becomes static.
You might think disabling all animations is too drastic, but to me with this type of decorative animations, it’s either all or nothing. We could’ve kept the floating animation, but that hints that the whole thing is interactive, which it wouldn’t be if reduced motion is enabled.
To disable the floating animation for example, we can pass shouldReduceMotion to the createFloatingAnimation and simply return the initial and animate keys to be exactly the same so no movement occurs.
```jsx
export const createFloatingAnimation = ({
to = 2,
duration = 2.5,
delay = 0,
shouldReduceMotion,
}: {
to?: number;
duration?: number;
delay?: number;
shouldReduceMotion?: boolean | null;
} = {}): FloatingAnimationProps | undefined => {
if (shouldReduceMotion) {
return {
initial: { transform: "translateY(0px)" },
animate: { transform: "translateY(0px)" },
};
}
// ... normal animation
};
```
```jsx
export const createFloatingAnimation = ({
to = 2,
duration = 2.5,
delay = 0,
shouldReduceMotion,
}: {
to?: number;
duration?: number;
delay?: number;
shouldReduceMotion?: boolean | null;
} = {}): FloatingAnimationProps | undefined => {
if (shouldReduceMotion) {
return {
initial: { transform: "translateY(0px)" },
animate: { transform: "translateY(0px)" },
};
}
// ... normal animation
};
```
For animations that rely on clicks or hovers, we can return early in those functions if shouldReduceMotion is true. This will prevent any animation from playing.
```jsx
const handleClick = () => {
if (shouldReduceMotion) return;
// ...click handler logic
};
```
```jsx
const handleClick = () => {
if (shouldReduceMotion) return;
// ...click handler logic
};
```
Below is the full implementation of the clock component with all motion disabled if the user has prefers-reduced-motion set.
You can test it by disabling reducing motion in your OS settings (macOS: System Settings → Accessibility → Motion → Reduce motion) or Chrome DevTools: open Command Palette (Cmd+Shift+P), search “Emulate CSS prefers-reduced-motion: reduce”.
## Optimizing performance
We animate a lot of elements at once. The floating animation of all 6 illustrations alone is constantly updating the DOM. That’s a lot of work for the browser, so you might see dropped frames. This might happen especially if you’re using lots of SVG filters.
Notice how many elements are constantly being updated.
We hit this exact issue, the animation felt janky. It didn’t drop to 5fps, but it was noticable, especially when some hover interactions were happening while all other illustrations were floating. The video below shows the issue:
Notice slight frame drops as the hover animation plays.
Usually when you hit performance issues with SVGs there are a few things you can do. Here’s what worked in this case:
```css
/* Performance optimizations for animations */
svg [data-animate] {
will-change: transform, opacity, stroke-dashoffset;
contain: layout style paint;
}
/* Optimize elements with expensive filters */
svg .filter-animated {
will-change: transform;
transform: translateZ(0);
}
```
```css
/* Performance optimizations for animations */
svg [data-animate] {
will-change: transform, opacity, stroke-dashoffset;
contain: layout style paint;
}
/* Optimize elements with expensive filters */
svg .filter-animated {
will-change: transform;
transform: translateZ(0);
}
```
will-change tells the browser which properties we’ll animate. The browser promotes these elements to their own GPU layer, which gets animated separately without recalculating the entire page on every frame.
contain: layout style paint isolates the element’s rendering – changes here won’t trigger repaints of sibling elements.
transform: translateZ(0) forces GPU layer creation as a fallback, especially useful for filtered elements where blur calculations are expensive.
Don’t add these preemptively, GPU layers use memory. Too many layers hurt performance instead of helping. Build your animation, test it, and only add will-change when you see dropped frames. We’re targeting [data-animate] specifically, not every element.
For a deeper dive into performance, you can view the performance lesson.
## Wrapping it up
The clock animation is complete. We went from a basic interaction to something that feels more intentional and polished.
The key additions:
- Subtle life: Floating and rotation make idle states feel alive.
- Better interactions: Hover debouncing prevents accidental triggers.
- Mobile support: Two-tap pattern handles touch properly.
- Accessibility: Respects prefers-reduced-motion.
- Performance: CSS hints for smooth animation at scale.
We only covered two of the 6 interactive elements in this illustration to keep this walkthrough focused, but you can view the full implementation of it on GitHub
---
# Rotation
- Module: Hero Illustration
- Lesson URL: /lessons/hero-illustration/rotation/
- Markdown URL: /md/lessons/hero-illustration/rotation.md
- Source mirror path: apps/anim/learn/hero-illustration/rotation.html
So, we know how to position shapes and animate lines. Now let’s rotate them. Click the clock below to see the hands rotate around the clock center.
## Overview
So, we know how to position shapes and animate lines. Now let’s rotate them. Click the clock below to see the hands rotate around the clock center.
Those hands use transform origins to rotate around a specific point. Let’s see how it works.
## The problem with SVG transforms
Let’s rotate a rectangle:
```text
```
It rotates around the corner instead of its center. This is because SVG transforms use coordinate (0, 0) as the default origin - the top-left corner of the viewBox.
You might try to fix this with transform-origin: center, like you would in HTML:
```css
rect {
transform: rotate(45deg);
transform-origin: center;
}
```
```css
rect {
transform: rotate(45deg);
transform-origin: center;
}
```
But in SVG, center refers to the center of the viewBox, not the element. So this still doesn’t give us what we want.
The real fix is to set transform-origin to explicit coordinates. For a rectangle with x="20", y="20", width="40", and height="40", the center point is at (50, 50). To rotate around that center, you’d write:
```text
```
This is exactly what we need for clock hands. Both hands rotate around the same point - the center of the clock - regardless of their own positions or lengths.
Let’s put this into practice. Assemble the clock pieces below so the hands rotate correctly when you click “Animate hands”.
Success criteria:
- Clock circle positioned at the center of the canvas.
- Both hands start at the center of the clock.
- Hands rotate around the clock’s center when animated.
```text
import "./styles.css";
import { useState } from "react";
export default function ClockAssembly() {
const [reset, setReset] = useState(0);
return (
{/* Use this button to replay your animation */}
);
}
```
## A simpler approach with transform-box
The explicit coordinate approach works, but it has a downside: if you move the clock or change its size, you need to recalculate the transform-origin coordinates. There’s a more flexible solution using transform-box: fill-box.
This property changes the reference point for transform-origin. Instead of using viewBox coordinates, it uses the element’s bounding box - making transform-origin work like it does in HTML:
```css
rect {
transform: rotate(45deg);
transform-origin: center;
transform-box: fill-box;
}
```
```css
rect {
transform: rotate(45deg);
transform-origin: center;
transform-box: fill-box;
}
```
Now center refers to the center of the rectangle itself, not the viewBox. Try moving the rect shape around - the transform-origin moves with it:
```text
```
## Challenge
Refactor your clock from the previous exercise to use transform-box: fill-box instead of explicit coordinates. You can post your solution in the Discord server.
---
# SVG Introduction
- Module: Hero Illustration
- Lesson URL: /lessons/hero-illustration/svg-introduction/
- Markdown URL: /md/lessons/hero-illustration/svg-introduction.md
- Source mirror path: apps/anim/learn/hero-illustration/svg-introduction.html
A while back me and dimi worked together on some animations of the illustrations on animations.dev. Hover on each of the boxes above to see the animations in action. This walkthrough will cover how we animated some of them.
## Overview
A while back me and dimi worked together on some animations of the illustrations on animations.dev. Hover on each of the boxes above to see the animations in action. This walkthrough will cover how we animated some of them.
Before we dive into the actual animations of that illustration, let’s cover some SVG fundamentals.
By the end of the three SVG fundamentals lessons, you’ll understand:
- The SVG coordinate system and viewBox
- Basic SVG path syntax
- Stroke properties for line drawing animations
- How to use transform origins correctly for SVG transforms
Let’s dive into it!
## Coordinates, not flow
SVG uses coordinate-based positioning. Every element is placed at specific coordinates using geometric attributes like x, y, cx, and cy.
In HTML, elements are part of the document flow. Block elements stack vertically, inline elements flow horizontally. The browser automatically positions elements based on the content around them.
SVG has no concept of flow. Elements sit exactly where you tell them to. If you create multiple elements without specifying coordinates, they’ll all render at the origin point (0, 0), stacked on top of each other.
Try switching between shapes and moving them around. Notice the differences in the coordinate arguments for each shape and how they behave:
```text
```
Shapes can also disappear. Try dragging the rectangle’s width or height to 0 in the demo above – it vanishes completely. The same happens with lines if the start and end coordinates match, or circles with a radius of 0.
```jsx
// These shapes won't render
```
```jsx
// These shapes won't render
```
The SVG spec calls these “degenerate shapes”. Shapes that collapse into a point or line aren’t considered valid and won’t render. This is different from opacity="0" or fill="transparent", where the shape still exists but isn’t visible.
## Making SVGs responsive
Now here’s the interesting part. Those coordinates aren’t pixel values. They’re values in SVG’s internal coordinate system, defined by the viewBox attribute. This is what we use to make our SVGs responsive.
Think of the viewBox as a camera looking at an infinite SVG canvas. It determines which portion of the coordinate space is visible and at what zoom level.
Without a viewBox, SVGs don’t scale properly. Try resizing the SVG below without viewBox, the shapes get cropped because their coordinates are fixed.
```text
```
Now toggle viewBox on and resize again. The SVG scales to fit because the browser scales the entire coordinate system.
The viewBox attribute takes four values: x, y, width, and height.
The first two values set the x and y coordinates of the top-left corner of the visible area.
The last two values set the width and height of the visible area. They determine how many coordinate units fit horizontally and vertically.
```tsx
```
```tsx
```
The viewBox above starts at coordinate (0, 0) and shows a 300×300 unit area.
Try adjusting the values below. Changing x and y pans the view. Changing the width & height will adjust the zoom/visible area.
```text
```
You can even animate these values to create smooth panning and zooming effects. Hit the “Animate” button above to see it in action.
In the hero animation, the SVG uses viewBox="0 0 622 319". The shapes work with this coordinate system, but the SVG displays at different sizes depending on the screen. This is why we can use consistent animation values at any size.
## Drawing with an invisible cursor
SVG paths let you draw complex shapes using drawing commands in the d attribute.
Think of paths like drawing with a pen. There’s an invisible cursor that starts at (0, 0). Commands move this cursor around or draw lines from its current position.
Use the arrow buttons below to step through each command and watch the path being drawn:
```text
```
This draws a square using two commands:
- M (move) - moves the cursor to a starting point without drawing anything.
- L (line) - draws a line from the cursor’s current position to a new point.
Path commands can be uppercase or lowercase, and this changes how coordinates work:
- Uppercase L 40,60: Go to coordinate (40, 60) in the viewBox.
- Lowercase l 40,60: Move 40 units right and 60 units down from the current cursor position. It’s relative to the cursor.
For a path that starts at M 30,20:
- L 40,60 draws to (40, 60).
- l 10,40 draws to (40, 60) as well, because 30+10=40 and 20+40=60.
Try adjusting the coordinates below. Toggle between L (absolute) and l (relative) to see the difference.
```text
```
Try it yourself: Draw a triangle.
Using the path command field in the demo above, try drawing a triangle. Start at coordinate (50, 30) and make it about 40 units tall.
Those of you with sharper eyes might’ve spotted something odd - both the triangle you just drew and the square from earlier have the same problem. There’s an awkward corner where the path ends. When a path’s start and end points meet without being properly closed, you get this visual defect.
Zoom in on the corner below to see it more clearly.
```text
```
The fix is the Z command which draws a straight line back to the starting point and properly closes the path. Click the “Toggle” button above to see the difference.
The Z command is handy because:
- It’s shorter than typing out the end coordinates
- It ensures the path is properly closed
- It works with both absolute and relative commands (though z and Z do the same thing)
So the triangle from earlier could be simplified to:
```tsx
M 50,30 L 30,70 L 70,70 Z
```
```tsx
M 50,30 L 30,70 L 70,70 Z
```
Or with relative coordinates:
```tsx
M 50,30 l -20,40 l 40,0 Z
```
```tsx
M 50,30 l -20,40 l 40,0 Z
```
In practice, you rarely write paths by hand. Design tools like Figma export them for you. But understanding the basics helps when you need to animate or modify them.
For a deeper dive into path syntax, check out Nanda’s A Deep Dive Into SVG Path Commands guide.
---
# Interviews/Dennis Brotzky
- Module: Interviews
- Lesson URL: /lessons/interviews/dennis-brotzky/
- Markdown URL: /md/lessons/interviews/dennis-brotzky.md
- Source mirror path: apps/anim/learn/interviews/dennis-brotzky.html
Brotzky is an engineer who loves design. He is also the co-founder of Fey, an app that helps you make better investments.
## Overview
- Henry Heffernan
- Mariana Castilho
- Lochie Axon
- Dennis Brotzky
Brotzky is an engineer who loves design. He is also the co-founder of Fey, an app that helps you make better investments.
## Emil (00:00)
Most people I guess know you from Twitter because you are in the design engineering-ish sort of category, I would say. But if you could tell a bit more about yourself and what you do right now, it would be great.
## Dennis Brotzky (00:08)
Mm-hmm. Yep.
Yeah, I’m Dennis Brotzky, co-founder of Fey. I run the engineering side. have two other co-founders, Thiago Costa, who does the design side. And then Thomas Russell, who’s a bit of a ghost, but might be making a Twitter account soon. He runs the engineering, product, admin, everything. He’s kind of a beast in our secret sauce.
## Emil (00:41)
Okay, so is it only you, Thiago and Thomas that is building Fey like full time? Because I knew the team was small, but I didn’t think it was this small.
## Dennis Brotzky (00:52)
Yeah, I think that’s what people are most surprised about when they think of Fey. It seems like we have a big team running behind it, but it really is only us three. know, I know we’ve had some discussions with like other companies and other people, and then they hear us when we say we’re only three people, they always assume we’re like offshoring some sort of talent or we have some secret people. But honestly, it’s just the three of us and we work
really well together and we all work incredibly hard. Just like the last month in March, we’ve had to take out a data provider and we’re like, okay, this has to get done. It’s like a company changing event in terms of significance. So we just work all day. Right now we’re working weekends. We love what we do. So it’s not really an issue, but
We just work hard, get it done. And that’s it. That’s kind of the secret. It’s just us three.
## Emil (01:59)
Is it just because you are working so well together and you don’t want to disrupt that? Or what’s the reason to not expand the team to five, six? It doesn’t have to get huge, but more people, I guess, more things get done to a certain point.
## Dennis Brotzky (02:17)
To a certain extent. mean, when we started Fey, we actually had six people. And slowly over time, we’ve kind of shedded people. each time that happened, we moved faster and faster.
The beautiful thing of owning a company and having it your way is you can make the rules. Right now we’re having so much fun just working between the three of us. I don’t know if you’ve ever worked with, like there’s certain people you work with in your career, in your job, and like you guys just work really well together and it’s so much fun. And you’re like, I can’t wait to build this thing with this person. Or it’s always like the first person that you go and get feedback from.
You run to them and you’re like, hey, look, look what I made. Do you have any feedback? Are you impressed? And like the three of us, we have that with each other and things are moving really well. And we just don’t want to, don’t want to ruin that. It’s just too much fun.
## Emil (03:20)
But does that mean that because the team is so small, you also get to design a bit? Does Thiago get to like code a bit or are the roles pretty strict?
## Dennis Brotzky (03:34)
they’re pretty strict. would say in the beginning, maybe I would be in Figma a bit more often and Thiago wouldn’t touch code at all. but more recently actually with AI and cursor and all the new technology around that Thiago is actually coding more than I’m designing. to be honest, I don’t do too much design or I just give Thiago some feedback and he just makes it look good. but
## Dennis Brotzky (04:03)
We’ve gotten so good at what we do and figuring out who should do what, where it’s the most efficient thing is he does all the design. I do all the engineering. Tom as well does pretty much all the engineering and a bunch of other stuff. So over time, our roles have gotten more and more defined where it’s just most efficient for Thiago to work. And also Thiago is like the fastest designer, I think, in the world.
If I think there was like a design competition with Brett and maybe lovable or something like that. And they had a 45 minute time limit. I was like, Thiago, you got to go into this because like he can spit out a beautiful landing page in 30 minutes. It’s absolutely insane. and he’s never a blocker in the company. He’s always like, our designs are years ahead and our engineering is fast too. But,
## Dennis Brotzky (05:04)
We’ve figured out the most efficient way to do it is having a bit more separate roles, which might not be very traditional nowadays where design engineers kind of are sitting in Figma. make the prototype, they do the engineering and they have like full control over that aspect. But I think because we’ve been working together so long, we’re almost like a single unit now and it works well.
## Emil (05:34)
Yeah, I think many people think that design engineers design and code, I like when I worked at Vercel and the design engineering team was pretty big, like we rarely spent time in Figma as well. We may need to code and maybe the animation side was the motion part was a bit where we had more or we did more work than the designers, but we rarely designed in Figma as well so I don’t think it’s that rare.
## Dennis Brotzky (06:04)
That’s yeah, that’s a very good point on the motion side. I think that me personally, I might have more say in the way it feels and it’s implemented. Thiago usually uses like a After Effects or just like Figma to roughly show or he’ll even just put still images in Figma of like, this is this beginning, middle, end. And then it’s very much up to me to make it feel good and make it smooth and make it possible in that sense.
## Emil (06:42)
To go back a bit, how did you end up at Fey? What did you do prior to that? How did you become an engineer in the first place?
## Dennis Brotzky (06:52)
Man...
Yeah. So I don’t have a traditional engineering background. didn’t do comp sci. I really didn’t touch a computer until I was, or I use a computer, but I didn’t code or do anything like that until maybe I was 21, which I think might be different than most people in the industry, especially if you were doing comp sci or engineering or something related in university. But I have a BA in psych, bachelor of arts.
and a minor in music technology. And there was one specific class in my fourth year of university where it was called music and the internet. And we had this kind of out there prof. It was a really small class of eight people. And it was supposed to just kind of be this, this fun course where I knew the prof, everyone’s having a good time. Uh, but randomly he showed us CSS and how to do a for loop in JavaScript.
It was just like the most random day in class. And he’s like, okay, guys, look at this website. And I remember vividly, he showed us a Coke bottle made in pure CSS on like some website. And he’s like, look how cool this is. And I was like, this doesn’t make any sense. Like what’s CSS, what’s so special about this Coke bottle? And he’s like, look, you can zoom in as far as you want. And like, it’s super crisp. It’s not an image.
And he kind of taught us the very basics of CSS and JavaScript. And then we had this one assignment where we had to make two websites for our favorite artist. So he’s like, pick your two favorite musicians and make them a website. We’re going to get 1 % bonus. And the class is going to vote on whose website is the best. So that was on a Monday. And I remember for the next week, I just dive.
dove so far into it where like my whole day was building this website. I remember the first time I dragged like indexed that HTML into my browser and I could see, oh wow, like this is how the internet works. It’s not like this huge black box. It’s just this file that you put into your browser. And there’s this stuff called HTML and there’s stuff called CSS. And if you want to make it interactive, you add JavaScript and uh,
I spent the entire week making these two awesome websites. And, in the end class voted mine was so much better than everyone. Like I was just in love with it. And this was my second last semester of university. And then I had like one more. So was like two halves of a year. And then I did comp sci one-on-one for my last kind of
course in university. And I remember it so bad. It was like all Java and I had no idea what was going on. I was in fourth year with a bunch of first year students that like already knew how to program and they’re doing all these assignments and I was just struggling so much and hating it to be honest. But I knew I was like, I have a BA in psych. And back then it was like,
I knew I wasn’t going to be a psychologist or something. And it’ll be very hard to find a job. And I decided, Hey, like web development, software engineering. It’s like very much in demand. So I kind of stuck through it, finished my comp sci one-on-one class just barely passed. And then I remember I got a part-time job working two days a week and it was awful. And I would spend all my other time reading books on JavaScript, making websites, recreating websites. I would like go to all my favorite websites and try and recreate them with HTML and CSS. And I like show all my friends and do all these things. And then eventually I was like, okay, I should make myself a portfolio website. And I spent so much time on it and I was so proud of it. And I sent it to a bunch of companies and then
first one I sent it to was Lightspeed, which is like a big Canadian company now, but at the time they were quite small. And the person interviewing me, he’s like, you’re very raw, but you look like you’re very passionate about this. And I can see like you just started a few months ago and you’re already doing like impressive stuff for that amount of time. So he hired me.
## Dennis Brotzky (11:38)
And that really changed my life. Like that’s where I met Thiago. He was the designer at this company and we hit it off right away. Cause it was, I think the first time he worked with someone that was so excited about everything and like he would design something and I would be so hungry to like see his Photoshop files back then. And he had like exclusively sent it to me. Cause he knew if he sent it to me, I would like make it right away and I would make it exactly how he had it in his head.
And he was, and he’d never worked with someone like that. I think before all the engineers he worked with would kind of butcher everything and make it. Alignments were wrong. The colors were wrong. Interactions were wrong. And then I would always be pushing him. So he was like, give me a design. would be like, what if we did it like that? That would be even cooler. And we kind of just like kept spiraling in this thing. And that’s how I met Thiago and like we’ve been best friends ever since. But then I moved back to Vancouver, uh, from there and I had to leave Lightspeed and I went to go work for a startup in Vancouver in finance, which is kind of where Fague comes in a bit. Uh, was a startup essentially, I was the first front end engineer to join the company and help them grow. But I would always be in contact with Thiago and,
We’d always send messages, DMs being like, I don’t like how this is done at my company. I’m like, yeah, like the designer here isn’t very good. He’s like, the engineers here aren’t very good. And eventually we just said, it, let’s make our own thing. So we started a narrative with two other people, Mac and Thomas, who’s our other co-founder of Fain now. And that’s how it all kind of started.
## Emil (13:34)
Is Lightspeed, this is kind of random but I’m not sure if I remember it correctly, is Lightspeed like an e-commerce brand or no?
## Dennis Brotzky (13:40)
Yeah, it’s a point of sale system.
Yeah. So they do. I remember when I was at Lightspeed, our biggest, I wouldn’t say our biggest competitor, but we’d always look at Shopify. So Shopify back then was also pretty early. Another Canadian darling, the best tech company in Canada. And then Lightspeed also was in that bubble at that time.
## Emil (13:42)
Right. But how did you start Fey or narrative like, and then Fey, did you like raise money or like how, cause I’m pretty sure it’s not like a VC backed company.
## Dennis Brotzky (14:25)
The way narrative started was a bit of an interesting story where we were all working full-time jobs and we didn’t really have the cash in our savings to just like drop everything and risk it all on this new venture. So Thiago had the great idea of, he was very in demand back then, and I’m sure he would be now, but he would get recruiters talking to him all the time saying, Hey, you should come interview at this company. They’re very interested in your design skills.
And I was like, Hey, why don’t I go to these interviews? And instead of them hiring me, they can hire narrative. And that’s what he did. He kind of went through a few interviews and as soon as the first person was like, we’re going to actually hire narrative instead of Thiago to do this design work. I think we all quit our jobs. We’re like, okay, let’s do this. Let’s dive in.
We have our first contract. Let’s, let’s go for it. And that was, I don’t know if anyone knows Hopper. was a long time ago. We did hopper.com as our first client. And we just kept the ball rolling from there and there and there. And then from there, we always knew we wanted to start a product. We knew consulting, you kind of get this like never ending cycle of trying to find your next customer. You always have to.
There’s no recurring revenue to put it simply. And that’s when we made enough, excuse me, enough cash from narrative’s consultancy side to start pay.
## Emil (16:06)
Do you now just work on Fey exclusively or do you do some contracting type of stuff sometimes as well?
## Dennis Brotzky (16:12)
No, definitely not. 100%, seven days a week all the time. We wake up, think of how can we make fate even better? That’s all we do.
## Emil (16:23)
Yeah, I think that’s also nicer about working on a product that you can... I feel like when you’re working for someone, you make something and then it ships and then that’s it. And with the product, you can keep thinking about how you can improve it and it’s a never-ending story and it just keeps getting better and better.
## Dennis Brotzky (16:43)
Oh yeah, hundred percent, we have this internal joke every time we’re like, Oh yeah, phase complete now. And then we’re like, we’re not going to have anything to build after these one or two features. And then lo and behold, every single time the scope just gets bigger and bigger and bigger. And we have more stuff to build and more stuff to build. So yeah, it never ends.
## Emil (16:49)
What makes people... I at least when I browse Twitter I see sometimes a post, a random tweet about Fey like praising something about Fey. And of course you... I kind of have an assumption why that is but you build it with just you know to other people like why do you think people like Fey and not some other tool? What makes Fey special.
## Dennis Brotzky (17:40)
I think in investing, it’s often overly complicated and very technical. I see it a bit like math. People are kind of afraid of it. And I think Fey is the best app in the world where it simplifies everything for you, making it like super easy to track your portfolio, do research and get all the analysis you need. And there aren’t many finance or I don’t think there are any finance tools that look and feel like Fey. Like it’s very approachable. It’s beautiful. Honestly, it looks very simple to most people, which is good. We do our very best to do that. And to be frank, two thirds of our team, like we don’t have finance backgrounds. We’re engineers, designers. Thomas has a finance background, but he’s like this weird amalgamation of finance, engineering and taste, which you don’t find too much in the finance world. And we really just build it for ourselves. Like we all invest, we all have ambitions to grow our investments. We want to have some cash growing. So we just build what we find useful and it seems to resonate with other people.
## Emil (18:52)
Something that like I’m kind of interested in, you’ve been working at for on on Fey for a long time. Don’t you get, you see that the same app every day when you work on it, right? Don’t you get bored of it, tired of it at some point
## Dennis Brotzky (19:26)
Yeah, but if you do, you can just change it.
That might be where being three people and kind of having full control over the product becomes a benefit where we launched Fade 2.0 January 1st, 2025, somewhere around there. And it’s completely different to the Fade that was before it. So it really feels fresh, at least in my mind, everything I work on. I think I also have the benefit of working with an amazing designer. So.
I am seeing what he’s building and like, I get hungry to implement it and make it a reality. And everything he’s doing is very exciting and beautiful, which makes my life even easier.
## Emil (20:15)
How do you guys keep the app fresh? Do you just like perform the design and build things based on your own intuition or do you listen or get users feedback? Cause the team is pretty small. Usually, you know, companies have even whole teams to do like do interviews with customers and whatever. Like, is it mostly you guys?
and your intuition or how does building a feature or updating a feature at Fey look like?
## Dennis Brotzky (20:45)
It’s a mixture of everything. I think the main driving factor is what we want to build for ourselves. I think if you’re building a product for yourself, you’re always going to find it very exciting because it’s what you want. But of course we get a ton of user feedback and we kind of have a high overview of what customers want. And we take that into account. It’s very important. We definitely listen to all our feedback, we have a feedback channel in our Slack, we have a ton of DMs with our customers where we’re always kind of bouncing off ideas. But ultimately, if we’re not excited about the feature, we’re not going to build it. It’s just going to be way too hard. It’s going to be like implementing something you’re not excited about is like the worst thing. And we do make sure.
## Dennis Brotzky (21:44)
We work on stuff that’s Even like we’ll be bouncing around ideas and being like, like what’s the priority? Should we implement A or should we implement B or C? And then our answer is almost what’s, what makes you the most happy? Build that. And that’s kind of how we keep it fresh. If you’re excited to build the feature, you want it for yourself, you’re to go into the nitty gritty. You’re going to figure out all the details.
You’re going to make it fast. You’re going to make it beautiful. You’re going to make it useful. And it makes it much easier.
## Emil (22:16)
Yeah, that’s true. When you are excited about something, then like you said, you go into the, to the, to the details and everything and it gets much better. yeah, that’s why, why I like, like building open source stuff as well. Cause I, I get to like, you know, come up with how, with everything and I only will build something if I’m excited about it because it’s my free time and I could be doing anything else. but yeah, but that’s, yeah.
## Dennis Brotzky (22:48)
You could be doing anything. Exactly. That’s where the magic happens though. Like when you have that excitement, you have this idea and you kind of have like a vague idea of how to implement it. You’re not a hundred percent sure, but you can kind of see all the parts in your head and you just go for it. And that’s where our best features have been shipped. That’s how we stay happy. And that’s how we’ve been working on FACE so long.
## Emil (23:16)
Yeah, I think you’re in a very unique position. know, the team being so small and you all having so much influence on the product and getting to work on the stuff you really enjoy.
## Dennis Brotzky (23:24)
Yeah. And, and that’s exactly what can answer your question of why our team is only three people. Because you only have one life. You might as well be having fun with your best friends, building a product you find exciting, doing what makes you happy. And that, that really is why we’re, three people.
## Emil (23:33)
Yeah, this course is about animations and people really like animations. You have posted a few stuff related to motion on your Twitter feed as well. How do you guys think about, let’s start with that. How do you guys think about motion animations at Fe? Is it something that should be built to compliment the rest or what’s the function of it at Fe?
## Dennis Brotzky (24:25)
It always has to enhance the product. It has to enhance the experience. It has to make it delightful to use. And that’s the primary driver of whenever we add motion. we just added in Fade 2.0, we added the dock. And that has a ton of little details of how fast it opens, the blur, it’s resizing, and it just feels really good to use.
And that enhances the product. makes people excited to use it. And that’s where we like to add in motion. also makes the product feel more alive. I think if everything is just super static, it feels almost too boring to me. I like to have motion to like indicate where things are going. I know like linear, for example, they had their composer box. It was like go into the side and it would teach people, hey, that’s where your draft is. Follow the UI. That’s a great example of motion. We have a few things like that in Fey. And in that sense, it’s amazing.
## Emil (25:42)
And when it comes to more complex motion, you mentioned Thiago designing like three states, for example, is that usually what happens? He designs like static states and you like come up with how it should animate to those states or?
## Dennis Brotzky (26:02)
Yeah, he gives a rough draft, I would say. He’ll use After Effects or just static states in Figma. And then it really is up to me to like implement it, make it feel nice. Like we just released a landing page, our new portfolio landing page that had like this expanding cards kind of springing into motion. And for that, he just put one little card and then he put like six cards stacked on top of each other. And I was like, okay, they should come up like this. But obviously it’s really hard to recreate that springiness in Figma or any tool. So it’s up to me to implement it, make it feel right. And that’s what I did. That’s what I do.
## Emil (26:52)
And how, how do you do use, if we get more technical, do you use frame of motion or I don’t know, CSS animations? How do you usually, what do you usually use? What’s your like stack?
## Dennis Brotzky (27:06)
My stack, I use React Spring just because I have been using it for years and years and years. It’s pretty much the same as framer motion or motion nowadays. It’s all spring based. I looked up both APIs. Motion is definitely the one to pick if you’re gonna do it now. But back when I started down that path, they were kind of neck and neck or framer motion was a very new, would recommend that one definitely. But I really think The secret to any smooth feeling UI, don’t use CSS transitions. Use a library that uses springs under the hood. That’s really what’s going to make it feel like butter. If you’re wondering like, oh, why is my, my button or whatever you’re working on, it doesn’t feel quite as smooth as this other thing. It’s probably because that other thing is using a spring underneath instead of a Bezier curve or just some other thing.
## Dennis Brotzky (28:12)
I don’t know if you’ve, have you found the same thing?
## Emil (28:16)
I definitely agree that I guess everything looks bad. Everything is a big word, but I think, know, very, very a lot of things look better with the spring curve. I think there are a few cases. are definitely like Sonar. I think I like the animation. I made it, but you know, I like the transition that Sonar has. It’s just the ease type easing, just the default ease.
## Dennis Brotzky (28:45)
The one I use all the time too. It’s the best one.
## Emil (28:48)
Yeah, but you know, I think it’s almost always better to use a spring. I wouldn’t say evreything looks... like transition, looks bad, but it’s definitely easier to make something look nice with a animation.
## Dennis Brotzky (29:04)
Yeah, I agree.
I think, especially if there’s motion, the spring will just take it to the next level. Like if you’re just doing a hover and changing opacity or color or something like that. Sure. She says transition is fine. But if you can put a spring into it, you’ll be very surprised how much better it feels. I know Sonar, for example, just uses ease, which is my favorite default transition.
And it makes a lot of sense if you’re going to do like a lighter weight library that everybody’s going to use. You might not want to add in a whole another library into your library.
## Emil (29:34)
Yeah, and it’s a very small animation, know, like it’s just, it definitely adds to the smoothness, but it’s not a very complex animation or something like that. I think, and it’s not as big, I think you see more difference in bigger sort of movements as well. Not a very tiny one. Well, don’t know, Dynamic Island definitely looks way better with spring animations. But yeah.
## Dennis Brotzky (30:14)
Exactly. That’s a perfect example. Pretty much any of the iOS stuff would not be possible with CSS transitions.
## Emil (30:25)
I mean, it is not possible with CSS transitions, you know, and a lot of CSS stuff is not interruptible, which is also a big thing that I don’t think many people talk about like page view transitions API is not interruptible, which to me is a, it’s a big thing when it comes to like making something feel good. If I use key frames for sonar, yeah, go ahead.
## Dennis Brotzky (30:27)
Honestly, bit of hot take, but I find almost everything implemented on the web just doesn’t feel that great. Like all the base APIs view transition. If you’re comparing it to iOS or Mac OS, these just, they’re so bad. Like I, have a new narrative.co website and built. It’s not released yet. It’s kind of been on the back burner because we have so much stuff to build, but, I use the view transition API there and it’s just. You can feel like an engineer implemented the API to kind of recreate the feeling or the functionality, but like the feel isn’t there and all the details. it’s that’s the very frustrating part about the web is it’s very limiting and you can’t always do what’s in your head. and maybe you can, but then it doesn’t work on a different browser or it drops a ton of frames. That aspect is very annoying.
## Emil (31:57)
Yeah, which is not to say that like, you know, view transition API or whatever is bad. It’s great that we get closer to like iOS like experiences, but it’s also what’s kinda, well, even if the technology was here, like it’s hard to, because you interact with your phone with your like finger, you know, and it’s much more intuitive and it just feels better because there’s like less.
sort of layers between you and the phone. And here you have a mouse which feels kind of disconnected from the screen. So I don’t think, like, I really like gestures on iOS. And I made vol, which feels nice on the phone, like on the web it’s kind of useless or kind of weird to use with a mouse. But I agree with you that we, iOS has a lot of, Swift has a lot of.
It’s also way easier to create animations they have built things to make that process way easier.
## Dennis Brotzky (33:03)
The most pain I know I’ll be in is when Thiago comes and he’s like, check out this animation I want you to implement. And it’s like something default iOS. I’m just like, this is not going to work on the web. It’s just not going to feel the same. Yeah. I’m very jealous of that aspect of Swift.
## Emil (33:17)
Have you tried coding in Swift ever or did you do only web?
## Dennis Brotzky (33:30)
Yeah, when SwiftUI came out, I gave it another shot. I just can’t deal with the feedback loop. It takes too long to compile everything. You have to set up Xcode. And it was just too slow for me. I’m too used to the web.
## Emil (33:51)
I’ve never, maybe I should do it at some point, but I’m curious how that is, but maybe that. So, yeah, I don’t know. did watch some, how do you call it? Like Apple’s presentations about the stuff that they release for it. It all seems very nice. It seems like they also have like presets for spring animations. I feel like built in like ease and ease in out, but then for, you know, spring animations type of thing. so you use almost exclusively spring animations then, unless it’s a hover background change or something.
## Dennis Brotzky (34:30)
Yeah, if it’s a key component on a marketing landing page or in-fe, it’s almost always using a spring. It just feels so much smoother to me. That’s all it comes down to.
## Emil (34:47)
And do you have like a few spring configs that you use or do you create a spring specifically for a specific animation?
## Dennis Brotzky (35:03)
Man, the amount of time I’ve fiddling with and tweaking springs is insane. So unless it’s the same component, it will have its own custom spring configuration. So intention, own friction, own mass and all that stuff. And I will obsess over that. I’ll change it by a hundred, change it by 10, change it by one take a recording of it, record my screen, play the animation, open the recording, go through the recording frame by frame by frame, see if there’s any weirdness going on, and then I’ll adjust it from there. Like for the dock on Fey.
You should have seen my desktop. had maybe 200 recordings of videos of like each frame of elements going in and out, blurring the timing of like stuff overlapping the springiness of when it closes and all those details. So yeah, definitely hand crafting each config for each element.
## Emil (36:17)
And do you, when you adjust the config, do you just do it and you kind of know what this change will do? Or do you have like a visualizer for, for this or, or do you just do it and see what happens?
## Dennis Brotzky (36:34)
No, think just over the years, you learn how it’s going to behave if you add more tension, if you add more friction, if you make the mass heavier, and you have a vision in your head, you’ll have a rough idea. But then just getting it all to work together. I just tweak it, refresh the page, replay it, tweak it, refresh the page, and on and on and on.
## Emil (36:42)
Yeah, that’s nice. good that you say that you record animation. That’s one of the tips from the course that I gave, because I do record my animations as well and replay them frame by frame. Just like you said, I think that’s really useful.
## Dennis Brotzky (37:15)
I think their DevTool has some sort of animation playback. To be honest, I’ve never really used it. I just find it easier to just do a quick screen recording.
## Emil (37:28)
The dev tools in Chrome, mean, or the motion dev tools? Yeah, I think in Chrome, it’s just like for key frames, I’m pretty sure, or CSS transitions, not for the custom stuff. But I’m pretty sure like motion, you are using a React Spring, so it’s different, but motion, I think has a Chrome extension that also lets you run it frame by frame. But I think a recording, even though we...
## Dennis Brotzky (37:31)
Yeah. Yeah. Yeah.
## Emil (37:58)
We maybe do have something like this in the browser. A recording is more, I don’t know, realistic or it might be placebo, but I like to see the actual thing, how it behaves on an actual, I don’t know, record, how, how someone would actually see it. Maybe it’s one-to-one with a DevTools thing, but I don’t know. No.
## Dennis Brotzky (38:20)
Recording is the way to do it. That’s all I’ll say.
## Emil (38:24)
But so you record a lot, like really. Every animation you make, you... Yeah.
## Dennis Brotzky (38:31)
If it’s a key feature or if it’s like the hero over landing page or it’s something that’s going to be used a ton. Yeah. I record it like crazy. It’s just the easiest way to see everything. Cause if you can go frame by frame and like, it’s really easy to go back and forth. Highly recommend recording it and tweaking it from there.
## Emil (39:01)
Yeah. You see things that you wouldn’t or you can also record things that you think look good made by other people to see, you to, kind of learn as well, I guess, even if it’s the iOS animations that we talked about, I think that’s a great way to learn as well.
## Dennis Brotzky (39:19)
Yeah, for I’ll, the amount of time that I have spent on my phone, just like dragging my finger very slowly, opening apps and like moving them with my finger, doing gestures to like fully understand how elements are blurred in, where they’re moving, where they’re coming from, the speed of stuff.
Or just testing the limits of like, I wonder if they thought of what if I hold my finger down, move it all the way and still have my finger down and then keep doing that over and over and over. And like the animation just keeps going, going, going, going, going, going, I’ve obsessed a lot over how iOS works, all the animations and the fluidity of all of it.
## Emil (40:00)
What’s like the biggest surprise or what’s the biggest... Maybe you found something that is repeated in all animations that makes it look good. I don’t know. What’s like the biggest surprise or something?
## Dennis Brotzky (40:29)
Biggest surprise, I would say is how simple a lot of the things are. Like I really liked the, how you can just close an app. You just flick up and I was always, when you see it in real time, you don’t realize just like the moment where the app changes to the icon. It’s very simple, but in real time, it just feels so good. The way they have like the app turning into the icon. I spent a lot of time. I thought there was some crazy secret sauce behind it, but it’s just in the end, it’s very simple. It’s a good spring curve on it and it feels really nice.
## Dennis Brotzky (41:16)
A lot of things are just very simple, implemented really well with very good configuration.
## Emil (41:26)
Yeah, I guess it’s a lot of trial and error as well in their case even. They try things.
## Dennis Brotzky (41:34)
Without a doubt, without a doubt. That’s how you get good at anything.
## Emil (41:41)
What’s like an example of good web UI. Cause Fey is considered, you know, I see Fey in like the same category as, you know, linear is a very well built software or stuff. And I think to build something like this, you have to have good taste, obviously. And what makes a good web UI to you? Have you seen something that you are impressed by? For example, yeah, I’m curious about that. Maybe you have some other app that you look up to or, you know.
## Dennis Brotzky (42:23)
Yeah, I don’t have any secret app that I look up to. I think to be frank, like most web apps are shit. the biggest thing about the web that I hate is jank and like when stuff moves around. So when something loads in elements move around and they go in different places, I absolutely hate loading skeletons. I think that was.
One of Facebook’s worst creations when they added the loading skeleton. just like ruined the web experience in my opinion. Linear is definitely well-crafted. It’s all on the client, super smooth, things don’t move around. We try and do the same in Fe, although we don’t have the same sync engine and full client side, but we do a lot of preloading to make it feel very seamless.
When you open a page, things don’t move around. There aren’t loaders. And if you can achieve that, I think you’ll be in the top, top 1 % of web apps. What you want to avoid is...
If someone clicks on your page, either a ton of spinners or a ton of skeletons appear, I think that completely ruins the experience. What you want to do is try and have the data already there. And as soon as the person clicks that button, it just loads in and the page doesn’t shift around.
## Emil (43:57)
What happens on the initial load then? Because that’s, there was no chance to load something before the initial load when you come into the page for the first time, like from a new tab or something.
## Dennis Brotzky (44:11)
Yeah. The initial, what do you mean? Like when you’re already in the app, preload it. But if you’re just going to Fade.com for example, yeah, okay. We have, we don’t show a skeleton. just show the shapes and they stay where they are with the data. I think that’s a huge aspect. If that makes sense.
## Emil (44:36)
Yeah, it does. Okay, so that’s the number one thing for you then. When it comes to...
## Dennis Brotzky (44:42)
I, yeah, to me, if you can master that, if you can figure out how to get your page to load in and say you do want to put a skeleton or something, make sure the content when it loads in is where it’s supposed to be. And it doesn’t like shift around. I think, that’ll make your experience way better.
Do get what I mean?
## Emil (45:08)
No, yeah, I get that. It’s just like at Vercel. did use skeletons as well. There is a lot of dynamic content and you know, like the width of some stuff, the width of the team’s name, for example, we don’t know how wide it’s going to be. So it has to shift horizontally, not vertically, which is not as bad. But like if you use skeletons, I think there is no way to do it 100% accurately, you know.
## Dennis Brotzky (45:38)
Exactly. And that’s why. I hate them.
## Dennis Brotzky (45:43)
I think, designed it in a way where it’ll work for any team name would be my solution to that. To me, that’s like an engineering solution thinking first of like, we have to put the team name there. we have to put a skeleton there because we don’t know it beforehand. Whereas a designer might think of, well, how can we approach this differently where we show something else other than the team name, maybe like an icon or something standard or we somehow do something. I can’t think of a solution off the top of my head, but that’s when having a really good designer helping you one-on-one goes a long way.
## Emil (46:25)
Yeah. But I agree that not having skeletons at all, like makes the process way, way more easier because skeletons themselves take a lot of time and you have to update them when the design is slightly updated and everything. There is a lot of solid, like it’s way easier to, and it’s also way easier to build from linear perspective. For example, it’s easier to build new stuff in linear because it’s built differently. You don’t have to think about. Things like skeletons, for example.
## Dennis Brotzky (46:54)
Yeah, data is, imagine if you didn’t have to think of the data loading in. From what I understand, linear does a lot of the caching upfront. So maybe the first time you load it. But I think they’ve gotten even smarter about that, where they’re breaking it up into different chunks, prioritizing. I’m sure they’re prioritizing the page you’re on. I’m sure they do a lot to make sure it feels super seamless.
## Emil (47:23)
Yeah, but you’re right, the only thing that could load a while or longer than instantly is the initial load. But everything after that is basically instant.
## Dennis Brotzky (47:23)
And it makes perfect sense for products like theirs that you always have open. It’s always on your desktop. You’re always going back to it back and forth. So, yeah, it’s good design and engineering from them.
## Emil (47:44)
Yeah. All right, There are a lot of people listening that either might have a job or are trying to get one. And even if they have, maybe they want to get better at what they’re doing or even switch jobs. Do you have any advice for people that are listening to? To that are trying to get better at mainly animations, but you know, just engineering or even design in general, any advice, something that you wish you have done earlier or things like that.
## Dennis Brotzky (48:22)
I think a big reason why I’m able to do what I do is because I really do enjoy it. I know it’s a very, maybe cliche answer, but if you’re not excited to be doing animations or getting better at engineering or getting better at design, maybe evaluate yourself first and figure out what you want to do. Because when you’re excited to build something, when you obsess over a hundred recordings of a single interaction or
You can’t wait to read the next design thing or you open FigMine, you’re super excited. You want to design something, you want to build something. Then everything just becomes easy. And I think people that are excited and passionate, they attract very similar people. and then that makes it very easy to find your next opportunity, your next job, whatever it is.
## Emil (49:23)
Yeah, I think I agree. I think that’s the hardest part of finding a thing you enjoy doing very, very much. And then after that, it goes naturally and you build beautiful things.
## Dennis Brotzky (49:33)
100%.
## Emil (49:44)
But yeah, I think that’s it. Thank you for your time and great answers. know, Fey is a great product and everyone listening should at least check the site out and maybe even the product. I think you guys are only in the US. Like I’m pretty sure I cannot use it in Europe.
## Dennis Brotzky (50:09)
Yeah, we focused on the US. We’re all Canadians ourselves. But US data is the most readily available. It’s the biggest market. So we’re focused on the US, but we have a lot of people asking for Europe. So we’ll see one day.
## Emil (50:25)
Yeah.
Would be great even to, for people to, you know, as an inspiration even, let alone the usefulness of the product itself.
## Dennis Brotzky (50:36)
Honestly, we have a lot of designers and engineers and just people in the industry. They go sign up, use Fey and then they’ll counsel and we always ask for feedback. And then it’s always like, I just wanted to interact with the app. I just wanted to learn about how this was designed. I wanted to give it a, give it a try myself and learn something new. We get that a lot. Yeah.
## Emil (51:03)
Yeah, that’s a great compliment, I would say. Nice thing to hear. If people sign up just to see how great a product is.
## Dennis Brotzky (51:12)
I mean, that’s the goal. That’s what we love to do.
## Emil (51:15)
Yeah, cool. I guess that’s it. Thank you again for your time. Is there anything else you would like to share or tell people?
## Dennis Brotzky (51:31)
I’m good. Thanks, Emil.
## Interview notes
- Brotzky’s Twitter
- Fey
---
# Interviews/Henry Heffernan
- Module: Interviews
- Lesson URL: /lessons/interviews/henry-heffernan/
- Markdown URL: /md/lessons/interviews/henry-heffernan.md
- Source mirror path: apps/anim/learn/interviews/henry-heffernan.html
Henry is a design engineer working at Vercel. He contributed to a lot of Vercel’s new marketing pages, Next.js Conf, etc. He also has a very impressive personal site. In this interview, we’ll talk about animations, easings, design engineer in general and how he got his job at Vercel.
## Overview
- Henry Heffernan
- Mariana Castilho
- Lochie Axon
- Dennis Brotzky
Henry is a design engineer working at Vercel. He contributed to a lot of Vercel’s new marketing pages, Next.js Conf, etc. He also has a very impressive personal site. In this interview, we’ll talk about animations, easings, design engineer in general and how he got his job at Vercel.
## Emil (00:00)
Hey, so this is the first interview for Animations on the Web. Today, we will talk with Henry from Vercel who works as a design engineer there. Henry and I worked together for a while on the design engineering team at Vercel. I’m a big fan of his work.
Henry, why don’t you tell a bit more about yourself first?
## Henry (00:24)
Yep - so I’m Henry Heffernan. I’m a design engineer at Vercel. I primarily work on the marketing related properties of Vercel, so things like our events such as NextConf, Vercel Ship. I work very directly with the design team and also some engineering to craft those experiences and create those websites as well as the actual Vercel marketing pages - like our Vercel homepage. Basically anything that’s not the dashboard or docs is something that I’ve likely touched over the last two years.
## Emil (01:13)
Cool - why the marketing stuff and not the dashboard or anything else?
## Henry (01:19)
I actually got hired to Vercel as a marketing engineer initially. I was “a marketing engineer” for about a year before transitioning to the design team and becoming a design engineer. However, like just being and existing on the marketing team before that really, paved the path for me to just continue doing that on the design engineering side of things.
There were a lot of really fun projects that we got to tackle when I moved over to the design engineering team - like the Vercel homepage redesign - which was super fun to work on and a like multi -month long project as well.
## Emil (02:04)
We are going to talk about the redesign later, but could we go back a bit? You obviously have a very impressive personal site. Many people on the course’s Discord were very impressed by that. Could you tell us a bit more about why you built it, how you built it, and how that happened?
## Henry (02:23)
The whole story started my last year of college when I was applying to jobs and I was really failing to get any offers because I was a pretty unremarkable graduate on paper, so to say.
So after submitting countless applications and attending a handful of interviews, I realized I should probably try and do something to differentiate myself a little bit, so I decided to start creating a website that would showcase my skills.
The idea for my website really stemmed from this vision that I had of wanting to have a 3D experience that people could go into and just see who I was, but in a very immersive way. Initially the whole setting and everything for this website was to be taking place in some cyber-punky future. But I realized shortly after starting that iteration of it that that would be nearly impossible on my pretty short timeline of trying to get a job before I graduated. So I did a hard pivot towards a 90s vibe and theme. That actually really worked out for me too, because the UI from the 90s is actually very simple and straightforward. So you can just make stuff really quickly and it looks period-accurate because it’s super simple.
After spending about two or three months on that project and pretty much doing only that and ignoring all of my schoolwork, I ended up releasing it to Twitter and it got a very positive response, which I was super grateful for.
I guess technically it’s made with just create-react-app - not Next.js or three.js or react-three-fiber. It’s pretty bare bones, which is just sort of a symptom of me being more new to the space at that time and not knowing that Next.js or react-three-fiber even existed when I created it.
## Emil (04:59)
How did you learn? Because this is pretty impressive 3D stuff that’s going on there. Did you like learn it at school or did you like take a course for it?
## Henry (05:16)
So I guess I learned a lot of 3D stuff in high school, just through self-exploration and learning things myself.
This is pretty deep, personal lore for myself… but I was active in some Call of Duty zombie-modding communities, where you’d create maps and stuff. I definitely learned some modeling then. Once you learn that stuff, you know how it works fundamentally.
So when it came to “I have to learn Blender, I have to learn all this stuff” - well, I did have courses at the time I was looking at, like Bruno Simon’s three.js journey that really helped learn the basics and everything.
I guess fundamentally having that background in just knowing how 3D stuff works in general really helped me create the experience.
## Emil (06:23)
Right. Because you did this to find a job basically and I’m guessing that’s how you got to work at Vercel, right? Because this is your first real job.
Did you apply? Or did someone reach out to you based on the tweet about the personal site?
## Henry (06:37)
Yeah, so when I “released this website” to my zero followers and I got a bunch of likes, I got reached out to a bunch - which was really cool - and I had the absolute privilege of being able to interview with a lot of companies and have my choices regarding who I wanted to go with. As opposed to the much more grim reality that I was facing before - which was like sending out a hundred applications and hoping to get two interviews.
So with that context - Guillermo, our CEO, actually reached out to me over Twitter DMs and that’s what kickstarted my interview process. I did still interview and I did several interviews with them before signing an offer. But that’s how I joined Vercel.
## Emil (07:20)
Right. Interesting. That’s how I joined Vercel as well, Guillermo DM’d me on Twitter.
So you work at Vercel. You switch teams - you didn’t work on the design engineering team from the beginning because there was no design engineering team when you joined.
Obviously a lot of people are interested in design engineering; design engineering is a very interesting career path at the moment. Many companies are hiring design engineers. Could you tell us a bit more about like how design engineering looks like at Vercel? How does your job look like?
## Henry (08:42)
I think at Vercel, the design engineer’s role is really just, as a whole, almost a replacement for what would otherwise be two people: a designer and an engineer both working on a project. So in any sort of case where you have that “iteration loop” of a designer and engineer, you can just replace it with one design engineer.
Because of that, we’ve worked on a lot of random things (seemingly). We’re kind of all over the place, which is very nice because novelty is always nice at a job and to get to do new experiences, not just feeling stuck working in one place.
To even like say that there is a single thing I do day to day, I don’t think that’s very honest. I do such different things every single day. It’s mostly just doing some designs, doing engineering, and owning a lot of the work that you do as well and not really having that back-and-forth that some people are so familiar with - where you’re talking as an engineer (or coming from an engineering background), that loop of talking to a designer, and going back-and-forth. That’s just not a thing that you have to worry about when it comes to the stuff that you’re doing. With that said, we do work with the designers as well and we do work with engineers to do our work too. It’s really hard to pinpoint exactly what we do, I guess.
## Emil (10:46)
But okay, so I obviously kind of know how it looks like at Vercel because I worked on the same team but -
## Henry (10:52)
I’m actually so curious what you think it looks like.
## Emil (10:57)
How design engineering looks like at Vercel or in general?
## Henry (10:59)
Yeah, yeah.
## Emil (11:04)
From my perspective, I think design engineers are people that sort of - you know - if you and I would work together on something, then we would operate on the same wavelength: I would like what you created and I hope you would like what I created.
Then the designers would also not have to give us a list with 40 - like it’s not that we built exactly what the designer wanted and there is no like feedback at all. There’s obviously feedback and we work together, but I don’t think that I could design and build something myself and be very proud of that.
I could say I’m a design engineer but I’m not a design engineer that designs and builds the designs that he designed. I know that there are design engineers that do that, but I would say that’s how I see it.
## Henry (12:17)
It’s definitely a spectrum of design engineers who lie more on the design side and the engineering side. For me, I definitely lie more on the engineering side, but I’m sure we can both think of plenty of design engineers who lie more on the design side.
Obviously, I don’t think there’s anything wrong with that - like everyone has their own strengths - I honestly wish I was more on the design side because I feel like that’s way cooler to be honest, but…
## Emil (12:50)
Yeah, yeah. And it gives you more, it gives you more power sort of, right? If you can design and build everything yourself, you don’t need anyone else. Like, like it’s pretty powerful. But I guess there are design engineers at Verso like that, right? I think yes, designs and builds her stuff. So.
## Henry (13:06)
Yeah, she’s super talented, yeah.
## Emil (13:10)
I guess at Vercel specifically there are many design engineers that focus on different things; like you and I code a lot, someone else designs a bit more.
One question that I’m very interested in because I think about it a lot and there is no one answer to it; taste is obviously something that gets talked about a lot when it comes to design engineering and people say that good design engineers have good taste.
How do you think about taste? How can someone learn or get good taste? And what does taste mean to you in terms of design engineering and everything around that.
## Henry (14:06)
I feel like taste is pretty difficult to define.
Even as like design engineers, I don’t feel totally qualified to answer this question. Because I feel like taste really applies to any profession almost.
It is a very important thing with design engineering, and that is what people really get drawn towards. But I don’t think it’s inherently a design engineering trait, if that makes any sense. I definitely feel like taste is much more of a designer trait that design engineers inherently hold having both of those skills.
So if you wanted to develop your taste, I’d say... the probably the number one thing is try and develop your design muscles first. It’s like breaking the problem down a little bit easier as opposed to just being like “taste is a magical thing that you just sort of have to have and some people have it, some people don’t”. It’s definitely a learned thing over time. Just based off of your experiences: what you do, what you make, whatever.
In terms of how do you refine your taste? You look at people who are really good. You try to educate yourself. Like there are books and courses on this. Your course does a great job, honestly, at trying to explain this very complicated topic.
Just finding inspiration for yourself, looking at new ideas. Even just being on Twitter alone, you already have an upper hand in terms of seeing what’s really going on in the UI craft/taste space. I open up my Twitter feed and I’m just bombarded with people making these gorgeous things and I’m like, damn.
The last thing is just practicing it like any skill. It’s just something that you have to do - over time, you’ll develop it, you’ll refine it. Everyone comes in with different tastes too, and everyone will have different tastes in everything too. So it’s not like something that you either have or don’t. It’s just something that you either have refined to be very accessible and people really are drawn towards it. I think that’s really what a refined taste is like. It’s just something that resonates with most people that see it.
## Emil (17:06)
Right, right. Yeah, I agree with that answer, but you said that taste is not necessarily a trait of a design engineer. So what is a trait? What are you looking for in a design engineer? Because you are obviously hiring at Vercel right now. What does make a good design engineer in your opinion?
## Henry (17:25)
Okay, so I do think that taste is something that a design engineer has, but I just mean it’s not explicitly something that the role has.
It’s just more so something that the design part of a design engineer has to have. So good taste is obviously something that we’re looking for. Or, at the very least, taste that resonates with us, because you know... to say one taste is good and one is bad is not too good to say.
The other things we just look for are just people that are really passionate about it. If you have the passion to create cool user interfaces or web experiences: that is number one best thing to see. And it’s honestly a requirement to be a successful design engineer in this space. And then beyond passion, just curiosity: wanting to learn, do more, and create stuff.
## Emil (18:30)
Okay, so we talked about your career, design engineering, and this course is obviously about animations and you created quite a few animations on the new Vercel marketing pages.
How do you think about animations? How do you build them? Do you just jump straight in the code and see what sticks? How do you think about easing, duration? Could you walk us through your process of building an animation and thinking about it while you do it.
## Henry (19:11)
The number one thing I try and do when I start an animation is to try and visualize it or see in my head what I want the final thing to look like. Just that exercise of trying to visualize something in its complete form.
That’s something that I personally tend to go towards in terms of iterating towards something that I want to create. Maybe this isn’t super applicable to people that don’t like to do that or aren’t good at that. I like to have a pretty strong idea of like what I want to do. And then I’ll just continue iterating towards that.
Most of the time that initial version I thought I wanted to create is not what I end up making. But even just having that strong vision to direct me towards something is really useful in just starting - it’s always hard to start doing something. But once you get the ball rolling and start iterating and get feedback on stuff, it’s way easier to keep going and making something great.
I do definitely jump right into the code, though, as the first step.
## Emil (20:22)
On those marketing pages that we see at least specifically, do you use like Framer motion for example, or do you mostly use CSS animations? What do you choose most of the time and why?
## Henry (20:41)
At Vercel, specifically just because we are so performance focused, we tend to really lean towards as much native CSS animations as possible. Just because Frame Motion is JavaScript, so you have to hydrate before you can start playing any animations. And also, they’re not necessarily hardware accelerated if you’re doing layout stuff.
Honestly, if you’re doing anything with layout, you have to use Framer Motion, basically. We tend to just go towards CSS animations, just because it’s super performant.
## Emil (21:23)
Yeah, yeah, I guess you also don’t do a lot of layout animations on those marketing pages, right? At least I haven’t seen a lot of those.
## Henry (21:31)
Yeah, and if they are layout animations, they’re usually basic enough to where you can really just fake it with transforms.
## Emil (21:37)
Okay, I see.
When it comes to easings - I have a set of easings that I use most of the time. And then I choose which one to pick. I rarely create my own easings.
## Henry (21:55)
Really? I always create my own. Yeah, cubic-bezier.com.
## Emil (22:06)
Okay, that’s interesting. How do you do it? Like with the visualizer?
## Henry (22:08)
Yeah, cubic-bezier.com.
## Emil (22:10)
But you kind of know which curve you want right? You don’t just drag around -
## Henry (22:12)
Yeah, of course. While I’m doing stuff, I’ll usually create four or five easing curves for something until I really dial it in and get it right. Because I like that level of touch and polish that like a very custom curve for looks.
## Emil (22:27)
No, that makes sense. But which type of curve do you usually go for? Or is it very specific or very one-off for each animation? Like ease-in-out or ease-out or do you just see what works each and every time?
## Henry (22:56)
I tend to lean towards, as of late, since I think my taste has evolved a lot. Like I used to really like exp-out where it’s super fast at the beginning and then it’s super exaggerated like this. I’ve been leaning a lot more towards just a pretty basic ease curve with a bit more exaggerated of an out. So I don’t know if you can visualize that.
## Emil (23:11)
So, faster at the end?
No, slower at the end.
So like an ease out, but a very subtle one, I guess.
## Henry (23:37)
A subtle ease in, a more aggressive ease out.
So it’s not symmetrical, of course. That’s definitely what I’ve been leaning towards. I really love that curve. I think they’re something very natural about it.
I’ve also been leaning towards - weirdly - linear easings lately too. At least stuff that just looks a little bit more linear, but it’s not linear. Just a little - you wouldn’t be able to know.
## Emil (24:20)
Yeah, that’s interesting. I don’t think I ever use linear unless it’s something like moving all the time like logos.
So you’ve obviously worked in the design engineering field for a while. What advice would you give someone who wants to become a design engineer? Like what would you do if you weren’t a design engineer and wanted to become one?
## Henry (25:06)
As a disclaimer, I personally don’t think I’ve been in this space for too long, first of all. But it is also a very new space. I’m incredibly hesitant to like prescribe a single path or even allude to there being a correct path to like go from engineer to design engineer, designer to engineer because that is normally how it goes.
With that said, I think one of the best things you can do is to just make stuff and get experience doing stuff, and then being very public about it. I think that’s worked out for both of us. I don’t want to sound too crazy being like “Yeah, just get super popular on Twitter. Then you can become a design engineer”. But that is a pretty valid path, honestly.
If you can gain a little bit of a following - just making cool things and sharing your work - that’s a very valid path to get this role.
Companies are also hiring. So if you have a strong portfolio: apply, see if you can get the job. And I also think f you’re currently employed at a company that’s interested in design engineering, you can try and push a little bit internally to get into that role.
I mean, you were the first design engineer at Vercel basically. Without you, I wouldn’t be a design engineer because you basically pushed us to create a whole team. If you feel like it’s valuable and you’re confident in your skills as a design engineer, then I’d say that’s also a pretty valid path to just try and do - but definitely up to the company and individual.
## Emil (27:13)
Yeah, I agree. You have to build stuff and let people know that you built those things. That puts you in an advantageous position because you don’t have to apply like you did to 100 companies - you will be approached if your work is good enough, obviously.
## Henry (27:41)
And I think too you have to just get better at it, obviously. If you are interested in it and you have a passion for it - if you just do a bunch of work and just keep flexing your design and engineering muscles to make new and exciting things, then you’ll get better over time.
## Emil (28:09)
Mm -hmm. I think the most important thing is just to build stuff because the more stuff you build the better you get at it, but that’s with everything.
When it comes to animations, how do you decide when to animate something and when not? It’s very easy to overdo it and animate a lot of stuff.
## Henry (28:43)
There are examples of people animating a lot of stuff and it really working out great and also examples of people animating nothing and it looking great. You have to use your taste to sort of dictate this decision.
But as a rule of thumb, when it comes to figuring out what you want to animate on a page… I come from the marketing side of things where you’re trying to tell a story. How do I tell a story with this animation? Is this animation even worth doing? Is it even contributing to that story?
Having that as a foundation to dictate what you do is helpful because it will avoid you over-animating stuff. Because I don’t think you should just go into being like, every single thing has to be animated. Because sometimes it really doesn’t and it really shouldn’t. So you have to be very conscious about what it is you’re doing.
If you ever find yourself just grinding out animations, maybe you don’t have to animate all that stuff. Just take a step back, think about it, figure out what you’re trying to actually tell and how you can use animation to accomplish that.
I would say do it tastefully. That is a pretty loaded phrase, but…
## Emil (30:08)
Okay.
Right.
No, but it makes sense. But you talked about like telling a story and that’s what Vercel wants to do with their marketing pages as well.
Is there something that you guys do to ensure that every page feels like Vercel? I’m talking about consistency in terms of animations. The AI page sort of feels the same as another page when it comes to the way everything moves and is animated or is every page just different?
## Henry (30:54)
At its core, we are always trying to tell the story of speed and performance and with that framework, it can help dictate how you animate things. And most of the time, we actually don’t animate a lot of stuff because it’s actually way faster obviously. And we don’t believe in super flourishy animations just for nothing.
## Emil (31:19)
Right.
Yeah.
## Henry (31:27)
We do sometimes go all out and animate some crazy things. And when we do do that, it’s really fun.
But it’s usually only to tell a story or if it’s like an important beat to the story that we want to tell. For example, a large globe in the middle of a page that is fully animated, and it’s actually telling this whole story of how Vercel works. It’s obviously worth investing into animating that whole thing because it can really draw people in and explain to people very visually what we do.
## Emil (32:05)
When you think about animation, is there something that you should never do in your opinion or rarely do or should avoid when it comes to animations? Like something performance related or easing related, duration related, anything.
## Henry (32:34)
Some actual performance tips are, you should never use Framer Motion to animate something coming in. If it’s something that should happen immediately on the page load, because that requires JavaScript to be loaded and everything so you can delay entire visual things just because JavaScript isn’t loaded. That’s one thing that I always keep in mind. I don’t know if there’s any hard rules you shouldn’t do.
What would you say?
## Emil (33:03)
What would I say? I don’t have anything on the spot right now, but what you said is pretty valid. I know we avoided that when I was at Vercel, you should just use CSS animations at that point.
Maybe one thing that I would say is to avoid linear almost always. I know you just mentioned that you like using linear -ish easing curves.
## Henry (33:20)
Yeah, linear-ish. Sorry, I want to clarify. I don’t like to use the actual linear curve, but I like things to almost look like it is, but it doesn’t look like it. I don’t know how to explain it.
There’s a very specific curve I have in my mind and what that looks like. But it’s more just feeling responsive and fast that I like from that curve, I guess.
## Emil (33:47)
Okay, yeah, that’s good. But yeah, linear easing is something that comes to my mind.
And not make your animations like too long, because that’s kind of annoying. Obviously, if it fits the vibe, or you tell a story, like you said, and it really fits this very specific situation, then yes. But in general, I feel like animation should be very fast to not become annoying to users.
## Henry (34:39)
There’s almost never a case that like animation - like a transition or something - should be over a second, absolute max. And that’s with a pretty extreme easing curve too.
I would say anything users clicking on - 200 milliseconds is the go-to you can even go lower like 150 for super fast stuff you want.
If you ever find yourself at 150 milliseconds for an animation, you’re like “this is not fast enough”, it is probably not worth animating at all. That’s always an option. You don’t always have to animate everything. I know that this is a course in animations, but sometimes the best animation is no animation. So something to keep in mind, I guess.
## Emil (35:19)
Yeah, that’s what Sam Selikoff said that to me about design as well: the best design is no design because the outcome has already been anticipated.
## Henry (35:49)
Same vibe.
## Emil (35:52)
To close this off, are there any people, books, blogs, courses or anything that you recommend to people that are interested in design, engineering, or design-engineering?
## Henry (36:12)
In terms of courses, I will be an absolute shill until the day I die for Bruno Simon three.js course because he really personally did a lot for me in terms of helping me progress my career. So I would say his course - if you want to learn anything about 3D, that is definitely the place to do it.
It’s honestly not that expensive and the amount of content he has - like 80 hours or something - it’s unbelievable how much he’s done. It’s absolute crazy value for what you’re spending. It’s like a dollar an hour for the price, which is insane.
So if you want to do anything with 3D, definitely check out his course.
In terms of other designers and engineers, I would say Twitter is one of the best places to be right now. I personally just follow a bunch of design engineers on Twitter. I love following their work and getting inspired that way.
## Emil (37:24)
So who specifically? Drop some names!
## Henry (37:26)
It’s an everyday occurrence that I go on Twitter and I’m impressed by someone’s work. So I really do not have anyone off the top of my head.
I can just start listing all the people I follow, but if anyone wants to know who I follow, just go and look at my Following list on Twitter.
A lot of design engineers typically don’t follow too many people, so check out their lists and you can just start looking at people’s stuff.
## Henry (38:02)
Okay, I guess that’s it for the interview. Is there like anything else you want to to say? This is your time.
## Henry (38:13)
I don’t have anything to push, right now.
I’m trying to post more things on Twitter. I have a few fun things lined up. Which is very exciting.
Follow me on Twitter, at @henryheffernan.
## Emil (38:26)
Okay.
Yeah, follow Henry on Twitter and and he will post some very exciting stuff soon.
So this was the first interview. Thank you for listening. And there actually at the time of this record of when this gets published, there is already another interview with Mariana, who also works at Vercel as well so you can check this interview out in case you haven’t already, but that’s it.
Thank you Henry for your time.
## Emil (39:09)
Thank you.
## Interview notes
- Three.js course by Bruno Simon
- Henry’s Twitter
- Cubic Bezier site
---
# Interviews/Lochie Axon
- Module: Interviews
- Lesson URL: /lessons/interviews/lochie-axon/
- Markdown URL: /md/lessons/interviews/lochie-axon.md
- Source mirror path: apps/anim/learn/interviews/lochie-axon.html
Lochie is a design engineer working at Avara. He built the Family site, and docs, ConnectKit, Avara’s site, and many other great things. In this conversation we’ll how Lochie got into design engineering, how he thinks about animations, and how he builds them.
## Overview
- Henry Heffernan
- Mariana Castilho
- Lochie Axon
- Dennis Brotzky
Lochie is a design engineer working at Avara. He built the Family site, and docs, ConnectKit, Avara’s site, and many other great things. In this conversation we’ll how Lochie got into design engineering, how he thinks about animations, and how he builds them.
## Lochie (00:00)
I started like with animation in particular, I started in school with like flip books, that’s like, I’ve always loved animation. And even before that, I had my dad’s. camcorder, and I would make stop motion with it.
We had to stop start the recording constantly, and I had little toys that I would animate around. So I’ve always been into animation in general, but my first experience with coding animation was, again, as a kid we had a computer. And I found, I don’t know what the software is called, but it’s a turtle that you can animate on the screen with code.
And that’s all you could do. Just draw. You can tell it to go north 30 pixels, south 40 pixels, and it’ll go up and down. And that just. Piqued my interest so much, but then nothing, there was no development for a good, like five or six years until I learned about Macromedia Flash and then that I would have been a teenager at that point.
And then, yeah, just started learning that and learning how to animate in that 2D software and then found out you can code in there as well. So that brought all that around. I went back to coding again, and then I started making some flash games and started posting them on newgrounds. com and did pretty well there for, especially for the eight.
Like I was, I would have been 15 when I posted my first like video game that I made. And then I started getting sponsorships for them and started generating some income and that was, that would have been. At the peak of like Flash games era. And then when I turned 18, I started applying for jobs, but that’s the exact same time I started applying for jobs using Flash, but that’s the exact same time when Flash started dying out.
With the whole Flash is dead thing where Steve Jobs just didn’t want, like he did a whole press release about not wanting Adobe Flash at that point. Flash Player doesn’t, he didn’t want that to exist anymore. It was pretty crazy to think this guy just kind of shut down his whole product. But yeah, so I had to pivot from Flash to HTML5.
Because that just started becoming a thing. This is a good 15 years ago now. 2000 and, yeah, 2010. So, yeah, so I had to pivot to HTML5 and learn but there was no CSS 3 animation sort of stuff yet, like anything that worked across browsers, so I had to do everything with JavaScript and jQuery only did a little bit of that sort of stuff that I wanted to achieve.
So I just started writing my own libraries and then I started working for agencies who saw my work. I did some smaller stuff and then I kept moving into banner ads and started making banner ads where I broke my own library because GreenSock was too heavy at the time because you could only fit a banner ad in 40 kilobytes.
So the GreenSock library was 8 kilobytes and that used up too much space. In my eyes, cause I wanted a high quality images. So instead I wrote my own managed to get down to zero 0. 4 kilobytes for just a really basic light version of it. Cause that’s all you really needed for that sort of stuff. And yeah, just managed to produce high quality banner ads, even though no one wants to see them.
I don’t think many people cared if they were high quality or not. So it was a bit disappointing to put all that effort. Into something that people didn’t want to see and then I’ve just basically been trying to chase a way to get out of that So then at some point I moved back to Video games and I started doing Mobile games in doing like triple a mobile games, and I just wasn’t a fan I thought going back to video games would have been great, but just wasn’t for me too much politics involved In that it just didn’t feel like it didn’t feel like they were trying to create a product.
It felt like more, they were just trying to make something that would generate as much revenue as possible rather than something that was as high quality as possible, which in turn would generate revenue. But yeah, so it just, it felt like a little bit of a, just what you assume, like the mobile game hype would have been a good.
just like everyone just trying to make as much stuff as they could. So I jumped out of that, couldn’t do that anymore. And then went back to some agency work. And then started posting some stuff on Twitter. Met a few people that were in, in the sort of startup industry and just had a few people shout my name out and then managed to find my way to meet Benji.
And then started working at family. It was a really, and it was like a good 10 years of not quite finding what I wanted. And then finally finding this sort of location, this sort of industry where they actually care about what you make and how you make it. And. Just all the user experience stuff behind it as well.
Just making sure that people enjoy what they’re actually using It’s like I love this sort of stuff way better than anything else.
## Emil (05:25)
Basically you got your job through Twitter or because you said you started posting and stuff
## Lochie (05:34)
Essentially, I got my work through Twitter. There was a few other things Associated to that because there was a couple of the other that came from people posting on Twitter.
So I would go join them and join those little social networks that were coming out. Like little apps. One was called, I don’t know if you recall, it was called screen wall. It kind of shut down a few years ago.
## Emil (05:57)
I might be too, too young for that. Maybe.
## Lochie (06:01)
Probably like three, four years ago it shut down. But yeah, it’s basically, so I was posting a lot on Twitter and I would just join these apps.
That people were making, and just trying to be social. I was just enjoying what people were making, and I just made a few friends. In particular I made friends with Pugson.
And he saw my work and just thought it was really well done. And then he started shouting me out a lot more, which then got me a little bit more momentum.
Until Benji picked me up at family, and we started working together. We did a couple of small things together for Honk. And then, yeah, we started working together full time. And I joined his team. Very, very talented team. And then we started making Family. They, they focused on the iOS app. And I focused on the web experience.
The Connect Kit, the Family website, and the Family Docs. And a few other things that we worked on that, cause we had to, we had to start a brand from scratch. So we worked on a bunch of different things and we, we settled on the family stuff.
## Emil (07:09)
How cause I guess you spent a lot of time on at least with family on something that is not the actual product, like the wallet, a site that you made for the web.
Well, like how important do you think the sort of packaging, I call it packaging, like the web, the website and everything around the app, how important is that? Do you think?
## Lochie (07:32)
I think it’s really important to have a well rounded-sm website because that’s the first thing that people see when they come to try to find your product.
It’s like, they’re going to curve to see. This app that people are talking about. And if they load up a page that is a bit lackluster, they might just kind of wave it off as something that’s still a work in progress or something that’s just not quite going to hit that bar of quality. When they hit a website that is a really interesting and high bar quality, they’re going to think that the app is just as better.
And I, when I made the website, I just wanted to make sure that it wasn’t extremely better than the app. I wanted to make sure that it was. a little bit lower in extravagancy than the application because I didn’t want anyone to go and download the app and find out that it’s got less going on. So I wanted to set their expectations high.
So then when they download the app and actually use it, they’re even more excited because it’s even higher than they thought it was going to be. So yeah, cause you don’t want to set the expectations really high and then under deliver on the other side. So, but I think having the website is really important.
It’s just, it’s marketing at the end of the day, but you just got to make sure that you’re marketing well.
## Emil (08:51)
That’s interesting. I didn’t think about it this way to like set expectations high, but not too high. You know?
## Lochie (08:58)
It’s under promise over deliver, is how I always put it. There’s a, there’s a good balance, and I think balance is very important across everything, especially like with animation, like you don’t want to go too crazy with it.
You want to kind of keep it subtle but keep it entertaining. It’s a really, it’s, it’s a line that needs to be carefully followed because it’s very easy to go too far in each direction.
## Emil (09:27)
So how do you follow the line? Cause like family and eighth size that you worked a bit on as well, like they have quite a bit amount of motion, like more than the usual side, I would say, but they are not crossing the line, at least not for me.
So, so how do you. You know, find that balance when you’re working on a site like family, let’s say.
## Lochie (09:52)
With most of the websites I work on My main focus is to drive the user’s attention and how they read the website. And if there there’s too much motion, you kind of take away their focus and they keep moving around. They don’t actually look at the content and you want to. So like if you have scroll animation when text comes in, they go to look where it’s animating in from, and if you animate too far away.
From where it’s going to sit, you’ve driven their eye too far away. And then they can get lost. Or it can just feel a little bit like a little bit disjointed. It’s probably a good word for it. So I like to, it’s called staging is the best way to put it. If you look into the 12 principles of animation, there’s a thing called staging.
And I think about that every time I make something. So for instance, the family website, the homepage, there’s there’s a bunch of illustrations that animate out from the center. But then I wanted the attention to stay in the central. So I actually animate them on a bit of a M curve, bit of a shape of an M.
So it starts from the middle, comes out and goes around. And then the text at the bottom appears after that. So it kind of draws the eye back up. So, it’s kind of like a love heart shape, if you can visualize that. But the whole point was to kind of like bring people back to the center where the content is rather than drawing their eye away from the content.
So there’s like a fine line to do that without doing it too extreme. It’s hard, it’s a very hard balance to find. But a lot of the time I just spend reiterating constantly until it feels right. And a lot of the time when it feels right is if I stop noticing what is animating, once it starts feeling like I can scroll and just feel like it’s a sense of flow and there is nothing pulling me too far or too, too up or too far down or anything that feels jarring.
And it might just be something that takes a while to figure out.
But, a lot of the time it’s just, As long as it doesn’t feel like it’s in the way, that’s usually the best animation that I like to pick. I want to be, I don’t want it to be in the way of the user, and I don’t want it to be in the way of the content.
I want to be a flourish to the content, and draw people back to what they’re actually supposed to look at. The, the issue I see with some websites is that they animate too much and they draw the attention away from the content and there’s some websites I go on and then I scroll down and I watch all the animations and it’s really cool and I get to the bottom of the page and I don’t know what the product is about and then that’s when I think that the animation has, it’s, it’s very, it can be high quality, it can be very interesting animation, but if it does not compliment the content, then it’s not doing a good job.
## Emil (12:57)
Do you like bring up or, or reference the twelve principles of animations a lot? Or is that the, the one you use a lot and the others not really? Or do you like, is this like some sort of holy grail for you or something that you reference a lot? How do you look at it?
## Lochie (13:19)
I think the 12 principles of animation, I feel like are a bit more of a guideline.
You don’t have to use all of them. But whenever you animate something, you are using one of them. So as simple as one of the, one of the rules is easing. Just like pacing for an animation, if it should slow down or if it should speed up is one of the principles. Another one is anticipation, whether it should start slow and then suddenly appear somewhere else.
So, and I think that they’re all important. They’re all pretty much necessary for animation to exist. So but in terms of referencing, it’s more that. I feel like it’s good to be aware of the 12 principles of animation, because then you have a bigger toolkit to reference. So I’ve been doing it for quite a long time that it’s kind of second nature to reference these, but I don’t feel like I’m constantly going back and looking at them.
It’s, it’s more just like being aware that they exist is what helps you. Being aware that the 12 principles exist helps you plan what you want to animate and in what sort of characteristic you want things to be animated. If you want something to be a bit more playful, you can introduce squash and stretch.
If you want something to be a bit more snappier, that’s when you can introduce more anticipation or quicker easing. And staging, I think is probably the most important one that a lot of people tend to forget, which is just how things are presented and what order things animate in and how you orchestrate your animations.
## Emil (15:07)
Is there like any, like other resource that you, I, for example, use I have a set of easings, for example, that’s one resource I, I use from, from Benjamin, the cock, he has I guess, just on, on, on, on GitHub. And I know you made the easings that that site, for example, but is there something else that you found useful in the past or ref go back to sometimes when it comes to animations, like some rules, I don’t know.
## Lochie (15:38)
I found that I have a set of animation springs that I constantly go back to and just linear animate easings as well. And those are the ones I put on easing dev. So the ones that I use a lot and I constantly. Need access to I just wanted a place to be able to go back and get them without having to go to a different project And copy and paste so I found just making a little resource was the easiest thing and then I’m like, well I don’t think that this there’s any special spring that makes your animation good because Every spring works for different animations so I’m like why not share these publicly and just let people have access to them because I still think that what’s important is how you use them rather than just having access to them because at the end of the day they’re just a couple of numbers and like if you have a really tight spring that works better on things like a hover but if you put that tight spring on something’s animating from zero scale to one scale, it’s going to feel a little bit too sharp and the same vice versa.
If you have a spring, that’s like a bit more wobbly. And you put that on a hover effect, it doesn’t work at all because way too much motion going on on a interaction. So I have a few springs and easings that I go back to pretty often. A lot of them have like a bit of a boiler plate. Template that I use for most projects and a lot of them kind of come pre packaged in that and I just reference them usually in a project I kind of work on I usually have the application Wrapped in a motion config tag where you can set the default easing across the entire app And that’s where I kind of, after I’ve started doing some animations, I kind of go back there and tweak the values a little bit until I feel like all the basic stuff feels covered.
And then I work off of that and tweak the values for all the individual animations that I want to fine tune.
## Emil (17:49)
So you’re like Use one spring or easing value in motion config and then make sure that most of the animations work well with that easing and then fine tune where needed.
## Lochie (18:00)
Yeah, that’s a good way to put it.
It’s, yeah, have one that you like to use. One that’s A good one is something that’s a bit more subtle rather than something that’s too extravagant. And then you can work your way up from there rather than having something that’s too extravagant and having to work everything back down. Because most things you want to be subtle and quick.
But then there’s usually the bits and pieces around different sections that you want to go a little bit more, a bit more louder with. And that’s when you can go to say a spring generator or easing dev for instance and tinker with values until you find the perfect thing.
## Emil (18:41)
Speaking of like finding the perfect thing when you work on a side, like family, like, and that’s what I’m kind of curious about as well.
Is, do you have a, like, is there are other people like. Helping you or would you be able to make a site or maybe you’ve done it like family on completely on your own or do you always like rely on someone giving you feedback on how it looks before it actually ships when it comes to motion
## Lochie (19:14)
Interesting So maybe we’ll do some background on how we built the family website. So with, with the family website, it was mainly between myself. We have our main designer, Alex and Benji, who was the CEO of CCTO. I don’t know if he was CEO at the point, but the process when we made the family. co website was we had, Benji and Alex, a designer at Hella Fee at the time, or Evara they made some static designs in Figma and I was also involved just giving them ideas and different suggestions, and I made a couple of different sections of the homepage.
We also had an illustrator come in and do different illustrations for the, all the characters and whatnot. And then once that was done, and we got signed off that that’s what we wanted, I then went and recreated it one to one. In, in, in react and then I just went ahead and animated things as I was building them and just the way that I thought they should be done and then I would get a little bit of feedback.
But for the most part, it was probably about 90 percent there. It was just sometimes it was just simple things like make that a little bit quicker or it looks a little bit different in the app, just like to try to make it a bit more one to one. But there are some animations that I made it too close to what it looks like in the app.
Yeah. That we actually wanted to style it a bit more for marketing purposes, rather than having it true to what’s in the app. And those were the notes that I wasn’t really ready for. Cause I was just like, I wanted it to be a bit more exact. And then, yeah, I got it. I got a great note of don’t try to copy, try to make it.
More of a flourish. So I went in and just like slowed down some things, made the movement a little bit more a bit more larger, but also not as sudden because the app is very quick with stuff because it, it can be because people just care about the end result rather than the process. And we wanted to showcase the process a bit more.
So I slowed things down, animated things a little bit more. Yeah, just a bit more bubbly, like a bit more a bit more overshoot in some stuff. And yeah, just tweaked some values just to kind of hone it in a bit more to make it a little bit more exciting.
## Emil (21:46)
And for something like more specific, like the hero, where you have the illustrations on the left and right, the dex in the middle, like from what I’ve seen, as far as I remember the icons, there is just one big SVG file.
Is that right?
## Lochie (22:00)
Yeah. It’s just one big SVG illustration.
## Emil (22:03)
Yeah. And, and I was wondering how, how does that look like in code? Cause they’re all animated. It’s each. Icon within the SVG like a separate react component that has its own like animations or yeah
## Lochie (22:16)
Yeah, so I have each each illustration each icon in that header.
I wrapped into another component so They all can time with a motion variant So they all have that offset animation and they all have this floating animation as well that are all offset to each other So they all have this kind of like constant motion and then Inside those, I also wrap the wrap the illustrations into their own component.
So then I can isolate that and animate them without getting too confused with between all the different SVG paths and groups. And then I just animate the groups from the top level in there. And I have all that wrapped in its own motion config. So then they all share similar values for their easing.
## Emil (23:05)
And do you just use like motion dot path or whatever, or do you use like the animate function or how do you animate them in general?
## Lochie (23:13)
90 percent of the time, I would probably just do a motion dot path or motion div. Just because I feel like for readability, it’s a bit more easier for people just to reference if they need to come in and see what’s happening, or if, say, another developer in the future needs to come in and look at it, or if I haven’t looked at it in months and I need to remember what I’ve done, it’s better most of the time to have it directly associated to what’s animated.
But the use animate function, I think is super useful for different sorts of animations. I just prefer doing things on the motion div level. The organization purposes sometimes I would do use animate because You can organize like if you’re animating between different paths, it’s sometimes a bit easier because then you have a bit of a messy Markup, but I think it’s that animation in particular.
It was pretty much all motion divs or motion paths motion groups Really? Everything I do is motion dodgy, right? I tried it I try not to animate the path or the shape too often. I try to wrap everything in a group So I’m never really directly modifying a illustrated component or an illustrator element.
Why? It feels safer and it feels like I’m not potentially destructing something. And it feels like it works a bit more in my head of kind of grouping something than animating that group. Rather than animating different parts inside a group. It also makes origins a bit easier to move as well, because sometimes when you have a path, the origin, the bounding box is a bit, bit messy.
So it’s easier to group it and then move the origin. I, I also have a I also have a global CSS I have a global CSS class sitting around. That’s just called like debug SVG. And if I put that class on an SVG, it then doesn’t outline and changes all the colors into a bit more of a debug mode. So then I can just add that on either an SVG or onto a group, and then it will highlight that group for me.
And then I can see that this is the group that I’m currently animating. And it kind of makes things a little bit easier to, to look at in your code when you, or when you work in multiple things, you can just search debug. In that file, and then just be like, cool, that’s the current thing that’s highlighted.
Now I can just work from there.
## Emil (25:44)
Interesting. While we are like on SVG animations, like, you guys have also animated a lot of, like, SVGs in the docs. Like, the icons in the sidebar and stuff. And like, those are pretty small things. I feel like, or not a lot of docs have those icons, like animated, like, I guess, I think I know the answer, but I’m curious, like why?
## Lochie (26:13)
Why is a good question. A lot of it has to do with. Not many other people are doing it. So first off we asked why aren’t other people doing it? And usually the answer is because they do too much rather than kind of keeping it subtle or keeping it quick So we animated a couple of them and we thought it was looking good.
And then we decided to do a bit more, more of them, just like some top level ones. And then we were like, yeah, this is working. So then we went ahead and did all of them. And we thought it was just like a really nice way to keep people entertained while going through something that can usually be fairly boring, like going through some docs.
Usually documentation is a bit more developer, well, it is developer focused. So usually documentation is pretty sterile, because it’s all about just giving information and just letting people figure out what to do with it. So we thought it would be a bit nicer just to keep some a little bit of entertainment in there So when people are navigating around this at that little bit of delight just to keep them happy and just keep people just entertained But nothing that’s in the way because you can use the docks without even paying attention to those animating icons at all. So if they were too large or too in the way people would start getting a bit annoyed.
## Emil (27:33)
Yeah I think so too docs are, you know, there to give you information and stuff, and animations could get annoyed, but those small SVG animations don’t really get in the way.
## Lochie (27:45)
That’s why like a lot of our docs don’t have any page transitions, because we want to be snappy, and docs aren’t marketing, they’re functionality.
So you don’t really want to get in the way of anyone. You want to just kind of give them what they want as soon as they can get it. But yeah, if you can have an opportunity to do something entertaining or something nice without getting in the way for those sort of projects, take the opportunity when you can.
## Emil (28:14)
Yeah. Docs are not marketing, but I’ve seen a lot of tweets about those animated icons. So they can, I guess, serve as marketing for your product a bit, you know?
## Lochie (28:28)
There’s that too. Just like there’s secondhand marketing that comes out of some, some of this stuff where if you do really good work, other people like to share it.
And, and usually, yeah, if you do something really good, people see it, they share it, they use it as reference. And then it kind of keeps the ball rolling. It’s like I haven’t shared a lot of the stuff that I’ve made, but I’ve seen a lot of other people share the stuff that I’ve made. And I think that’s, to me, that’s actually, it’s quite nice to see other people celebrating something that I’ve made.
And I don’t really mind if I’m not referenced in or like credited in anything. I just kind of like personally, I say it is cool. It’s interesting.
## Emil (29:14)
Think you, you should share more, but
## Lochie (29:18)
I should, yeah, I should, I, I definitely tell people that they should share more. But I should practice what I preach and I should also do more building in public. I have a lot of side projects that I work on that kind of fizzle out because I never find like a backend developer to help me out.
But I know how to do backend development. I just don’t enjoy it as much. So it’s like, I need to be more okay with. Doing the back end development as well. So I can just get things launched and ship more things.
## Emil (29:51)
Anything else about
## Lochie (29:52)
sharing some more stuff? I’ll start sharing some stuff.
## Emil (29:56)
Well, while you work on those animations, like for family or whatever cause the design aesthetic, like you said, like, how do you, where, where does the inspiration for motion come from? Like, how do you figure out how to move certain things for family
## Lochie (30:14)
or just in general?
## Emil (30:15)
Just, just in general, like how do you think about when you see a static thing, like how does that, you know, form in your head? Like, it’s kind of a hard question, I guess.
## Lochie (30:27)
Yeah, it’s, it’s very. instinctual to me, I think, cause I’ve just been doing it for so long that I have so many different reference materials in my head of just the things that I’ve looked at. Like, I’ve spent a lot of time just looking at things and like, whenever I see like any, like if I look at an advertisement, like a billboard, the first thing that goes through my head is how did they put that together in whether Photoshop or whatever, and like.
And then I also think like, how did they get that photo of that car in that location? Or like if I watched an animated movie, I’m usually trying to figure out how they did the composition and how they put things together and why they animated something this way and why they did something that way. Same thing with film.
Try to think about. Why did they choose that shot over that shot? Why they choose to put two people in the frame instead of one person in the frame? It’s because a lot of that can it’s just it’s a type of design language And I feel like when you do a lot of different things and when you experiment with a lot of different ideas and you look and you absorb the things that you are consuming, you kind of start understanding it a bit more and getting these like instinctual thoughts of like how things should go together.
So like when I saw the family designs and they’re in front of me, the first thing I thought was they’re, they’re quite playful, but they’re also Not like they’re serious, but they don’t take themselves too seriously. That’s how I saw it. So we can do animations that have a bit more balance and a bit more momentum, a bit more squash and stretch, and it can be fast, can be a bit slow.
It can do a lot of things. So my initial thing was I animated the I’m just trying to think of what I did animate first. I think I animated the, hang on, I’m just going to go to the family website. Try and remember what I animated first. It was before we had the characters. Oh, okay. The first thing I animated on the family website was the backup now animation.
And originally I had it when I first made it, I had it really bouncy, not super bouncy, but just enough to have a, like a bit of overshoot. And then I looked at it, I just spent some time looking at it. I’m just like, it doesn’t work well with the text there. And it also doesn’t fit the vibe of something that’s supposed to be snappy and supposed to be quick.
So, so I’m like, okay, I’ll just get rid of the the bounce and just have it just ease out. And I felt that that was too static. It just felt too, too basic and not snappy enough. So I really honed in I just use a, an easing function rather than a spring for it, but I just honed in the values until it was just like the tiniest amount of overshoot.
I think the last value is like 1. 005 or something. It’s like really, really. enough but it’s enough to have like two pixels or one or two pixels of overshoot just enough to have it a little bit bounce because it moves so fast. So when I, so usually I would experiment with a few different variations of what goes through my head of I see a design how would I animate it let’s just go ahead and start messing around with ideas and as I’m animating something I’m usually tweaking values as I’m moving elements around being like that’s too fast that’s too slow or that’s moving too much overshoot not enough spring not enough easing So it’s a constant, like, it feels kind of like I’m cooking and like, I’m constantly having to change the, like, add a bit more salt, add a little bit less of this, add a bit more pepper, like just constantly moving some like stuff around until it starts feeling like how I want it to feel rather than following a strict recipe.
## Emil (34:38)
Because I have to go away from the screen or at least from that animation to like see the flaws again after a bit of time. Do you like code an animation like one go or do you like come back to it later and see something new that’s like not great?
## Lochie (35:01)
I would say over the course of a project I probably revisit an animation once or twice. But usually I would go in, animate something, I wouldn’t stop until I’m happy with it, and then once I’m happy with it, I don’t try to spend more than a day animating something I usually try to get something done in an hour or two, and then once I’m happy with it, I kind of go, okay, cool, that’s done, I can move on and keep doing other things, I would rather Keep moving.
And then as I’m working on a project, cause you’re always moving around the project, trying to like look at things. And if something catches my eye, that’s not quite right, I’ll go back and change it and I’ll tweak it. And then it’ll be like, that doesn’t look quite right. Or it doesn’t match this other thing I’ve animated now that I’ve kept the project moving.
So then I’ll go back and match values or tweak. Things constantly. So from the first time I animate something to when something is released, I probably iterated on it 50 times, but they’re all little tweaks that I just go back and change the value, continue on, but I think it’s fairly rare for me to need to step away from something completely because I like to just keep moving and if I step away from something completely, I tend to think about it too much whilst If it’s just something that’s constantly, I’m constantly looking at, eventually I’ll find the flaws just by experiencing it.
And like for the, for instance, the family header animation with everything kind of blooming out, at some point I thought things were moving too fast. So I went and slowed everything down a little bit. And then I realized that some of the elements were coming in too quickly in the order they, they blew me out in.
So I just changed that specific elements delay a little bit more and just kind of like slow tweaks. And it was just the idea of just constant iteration on if I noticed something I change it and rather than do it once and think that it’s done or do it a few times. And if I do modify something, I usually just tell the team.
I changed something, but usually if it’s something so small, I don’t even worry about it. It’s just like, I noticed it. If I noticed it, it’s probably wrong. So,
## Emil (37:27)
And this has been a very specific question, but, but I’m like, personally curious about it. Like. Everything comes in with a bit of a different delay in the family’s header.
Stuff is, is that, is every delay like hand may, or is it like incremented by the index of each like SVG or how, how, how do you handle those delays?
## Lochie (37:48)
I have everything wrapped in a group. And I do a variant with a delay on it, so stagger children, and I have that on the left side and the right side, so you stagger children for that.
And then I have to manually order things in the order that I want, but I have to move the SVG paths around, but because I have everything in individual components, I can just look and go, that’s that star. That’s animating. Just move that component, move it up a couple. So I have to sit and reorder the react.
Oh, I have to sit and reorder the elements within that group to get that order. But most part is just a stagger children. And then the ones that, there’s a few elements that animate in together. And I just take that out of that list and add a manual delay to it.
Or I group that again. Depending on how I want to do it.
There’s a bit different. So some, there’s a couple of stars that animate together that aren’t one group in the SVG file. So I just grouped them again and use the variant with a motion group and then let the variant animate them together. But the problem with when you do that is that the origin point is now centered to the three different elements.
And I want each one, each individual element to animate in. From their center point. So rather than like, does that make sense?
## Emil (39:11)
Yeah. Yeah.
## Lochie (39:12)
Yeah. So instead I took them out of that variant group and just added a delay on each one. Just so they all animated from the center because I don’t want them like it was easier to just group them all, but it didn’t work that way.
So I added a delay manually. So some of them are manually orchestrated, but for the most part, it’s just a stag of children with some tweaks.
## Emil (39:32)
Interesting. Yeah, I guess we we’ve been talking for a while, but I guess I have two two last questions So first first is like is is there any site or app or whatever?
That is not like family related that you are really like impressed by it’s motion animations or yeah transitions
## Lochie (39:53)
I’m really lucky to work at a place that I’m inspired by That’s like, I’m very, because I work on the web and they’re working on these apps, I get inspired by what, why I get inspired by what they’re working on.
And then it, that helps a lot because I work, I work with some very talented people and I’m very happy with, with my team. But when I started working here, they had already made the honk app and that’s, that was just a level beyond a lot of apps that existed at the time. And then when I came in, I knew that I had to hit that bar of quality.
So when I made the Honk website, I was just trying to get to that level. And I did have to upskill a lot because I wasn’t really familiar with like React at the time. So I was fairly new at it. I had to move from just traditional CSS animations to, I didn’t even know motion. I didn’t even know frame of motion at the time, so I like, that was a whole new thing for me.
It’s like, so I jumped in, learned that realized how good of a library it was and have started using that as standard. Although I still do CSS animations every now and then like I’m really inspired by one, the team I work with, which I think is great, but I’m always constantly looking at different design references and I’m bookmarking things constantly to look back at, because I find that when I start an animation, I try to do things my own way, but then sometimes.
I do need to look at references or I remember a reference that I’ve looked at and I just want to go check it out again. So it’s really good to have some sort of bookmarking reference like arena is really good for that as well. Just being able to just grab a link and just put it somewhere and think about it later.
And I’d also have a folder where I just screen record things. And just be like, that’s cool, I’ll just screen record it, save it, and then I might look at it later. I feel like the act of screen recording something kind of solidifies the, what it, like, kind of makes me remember it more, because it’s so rare that I’ll screen record something that I see.
So, All it takes is to screen record it, save it, and I don’t really have to look at it because I’ve already remembered what I screen recorded. It’s a really weird like, way that my brain works, but, but if I just save a link or bookmark something, I tend to forget that I bookmarked it. So, for some reason that it’s, that just makes you remember, like, I remember, like, if I take a photo with my phone, I’ll remember the photo I took, or with a Polaroid or whatever, so.
Sometimes it’s good to take a photo, save it, and you don’t even have to look at it, but it helps you remember that it exists because you actively did something rather than just press a bookmark link. So, yeah, but if you want like direct references of like things that I’m inspired by, I’m, I’m Inspired a lot by just other developers on Twitter, really.
Like other design engineers. And like, that’s why I like Twitter so much. Like every time I open Twitter, I usually get inspired by something. I usually look at something and go, that’s really cool. Or someone releases a website and I open it up and I’m like, Oh this is really cool, how do they do it? And then I just inspect Element and just look at the different, the code that’s there.
And just try to reverse engineer whatever they’ve done. To understand how they’ve done it and that’s that’s how I would say I learned everything I know is just by like looking at other people’s code by inspecting element because I’m self taught and Everything is just like if I didn’t have inspect element.
I probably wouldn’t know what I know today So just that one tool I think is the most useful tool you can have is just knowing how to look through other code It is getting more difficult now with frameworks like compressing everything and minifying everything But yeah, it’s sometimes really good just to go through, especially if it’s a CSS animation, it’s just all there, just sitting there.
So JavaScript animations are a little bit harder to deconstruct, but possible, but more difficult.
## Emil (44:09)
When it comes to like CSS and JavaScript animations, you said you use CSS animation sometimes. When do you use CSS animations or how do you decide when to use what?
## Lochie (44:22)
Usually it would be when I try to choose between JavaScript or CSS animations, usually it’s got to do with performance or it’s got to do with developer experience.
So if I’m working collaborative with collaborative, sorry, if I’m working collaboratively with other people, I usually opt for JavaScript animations because it’s a bit more easier to read and less to kind of decipher. Because other people might need to come in and modify something, but with with CSS animations, sometimes they get complicated if you’re layering different animations and different different key frames and all these different sort of things.
But that’s specifically for. animations, like SVG animations, if, if you’re doing like a hover effect, I try to keep them to CSS only like transitions. So if it’s a transition, I try to stick it to CSS. If it’s an animation, I then try to figure out what’s best performance and what’s going to work best for that use case.
And if it’s going to work best for our team as well, because It can be more performant, and if it’s just, like, a tiny bit more performant, is that really a better payoff than someone coming in and being confused by what’s being set up. So if, because like frame of motion is really good, we’ll just motion now, but yeah, frame of motion I think is really good at just handling its own performance.
So like when you scroll out of view, it tends to not calculate it as much. So you don’t have to think about that sort of stuff. CSS does that too, but it’s cause it doesn’t paint it, but it’s more just don’t have to think about it. So that’s, what’s good with frame motion. You just don’t have to think about all the bit of performance behind stuff until you do things that are over the top.
But if you’re going that far, Then you should probably look into CSS animations because they are native. So you get a little bit better performance
So I use yeah, I use CSS purely for performance reasons or for interactions any sort of interactions I’ve tried keep it as CSS
## Emil (46:35)
Right but if you had to like my guess is that you usually or use frame of motion or motion most of the time
## Lochie (46:46)
I would say 80 to 90 percent of the time, except for things like a button hover or anything.
So, any sort of graphical animation would be frame of motion. Any sort of like, if I had a dropdown, I would do it with CSS, because I feel like that’s a bit more just performs a bit better, but if I want an interruptible animation, I would use frame of motion because you can’t really interrupt a CSS animation.
You can interrupt a CSS transition, but it depends how you animate everything because animating everything with a transition can start getting messy.
## Emil (47:20)
Yeah, yeah, I use CSS transitions for for, for my toast library because they were interruptible because I use CSS animations at the beginning, but then when you added to toast very quickly, like it would jump to, to the new position and stuff.
So, but yeah, I agree that they can get messy. So I’m very thankful for frame of motion.
## Lochie (47:41)
The cool thing now is that. CSS has spring animations, so you can actually do spring transitions as well. So that’s where it’s starting to get really interesting. Cause it’s an interruptible spring, but the spring is actually a linear animation because of the way CSS does it.
So it’s a static spring that is interruptible is a really weird visual when you see it, but it can work for different techniques. So I’ve used it a few times and like, it’s very niche, but it’s a cool little tool to have.
## Emil (48:13)
Yeah, but it’s my, my, my thing with like CSS Springs is that I don’t think like I would always if it had no like drawbacks, like I would prefer to use like frame emotions springs over CSS Springs.
## Lochie (48:30)
Yeah, same. It’s, it’s all depending on like, I think it’s very rare that I’ll use a CSS spring, but I am finding them like, like I’m currently looking at the connect kit library that I built and the biggest dependency we have in that is frame of motion and I’m thinking I can remove that and lower the package size a lot by just introducing CSS animations like only And I know there’s like framer motion has like an M function for like lower, like a light version of motion.
But it doesn’t reduce it as much as I was hoping it would reduce it. So, I’ve been considering removing Frame of Motion entirely in that project, just to replace it with all the CSS animations. But there is an animation in there that’s fully interruptible, and I do need to make sure I can make that interruptible as well, and it has springs.
So it’s like, now that CSS springs exist, I can make, I can redo that entire animation in CSS only. And that’s where I think it’s interesting when you’re looking at performance, and looking at bundle size and all this other sort of stuff. Like, that’s when it gets really interesting. But I would say for anything like marketing and stuff like that, just frame of motion is easier.
It’s easier for everyone as well.
## Emil (49:44)
Yeah. Yeah. Yeah. All right. I guess last question to close it off. There are obviously many people listening that are trying to, I guess, get to like your level and. While you are not sharing that much, I know how much, you know, great work you’ve produced. Thank you.
And I’m wondering whether you have, you know, any sort of words of advice for, for people that are trying to get better at.
## Lochie (50:15)
Animating things.
There’s a lot of different advice I could give. There’s, it’s like, if you’re still fresh, I feel like it’s good to just look at things and just try to understand how things move and kind of like commit a lot of things to memory of just like how fast things are going. And like, especially like in the real world.
Like how fast something rolls across a table, how fast something like falls off a table can really help you kind of like tangibly put motion together. Cause I think like some of the best motion works when it’s tied to a real value in real life. So basically physics, but yeah, so it’s like a lot of, if you go too far with animation, like too much spring, it can feel not grounded.
And that can be good, but that’s when you start getting into like, Looney Tunes sort of space. So, if you can find a way to look at the world and try to figure out how everything is grounded, and how everything moves, and then try to translate that into motion, like, on the web, or any sort of projects you’re making, it can kind of help translate things in a more, like, the viewer will see something that’s translatable rather than something that’s not existing in this world.
But that’s one piece of advice. Just like look around and just like pay attention and just like, just look at things and spend a little bit more time just looking at things that are moving and just understand it. A lot of things like another one is like watch film, watch TV. Watch animation and just like but don’t like don’t just sit back and consume it try to actually pay attention to how they made it And like why they chose certain things and how they framed a shot and why something moves a certain way Because a lot of the time they think about a lot of this stuff, especially in film and If they’re thinking about it and you can kind of break reverse engineer and break it down why they do it It can kind of help you come up with reasons and logic to do certain things.
There’s also a lot of YouTube channels that do a lot of that stuff. Some of my favorite ones are like Every Frame a Painting, and Nerdwriter is another one that I like to watch sometimes. And they explain like different things in film. And break down like why certain things were done certain ways. And I feel like that translates to motion as well.
Just kind of seeing something, trying to figure out why it was done that way and not done in other ways. And kind of like figure out the reason why whoever made it did it that way. Helps you understand why other things should be made in certain ways. I would also suggest learning, learning as much as you can in different tools also helps because coding animation is one thing, but then there are other tools that help you get prototypes and concepts across, across, like Rive is a new one that’s kind of up and coming.
And sometimes timeline animation can help a bit more like visually just by moving a box around. And. The usual animation test that everyone starts off with learning is a bouncing ball and just different animating a ball bouncing across the screen and animate it in 10 different ways. Whether it’s fast, slow, bouncy cartoony or like moon gravity, like animate it in so many different ways and then you can kind of understand the different, the different texture of animation you can do for the same animation.
And then from there, pick which one that you find the most appealing and then animate that style again 10 times for the bouncing ball, but in different variations. And then you’re kind of tweaking and honing your taste for a certain type of animation. And then the next thing you can do from there is choose the one you don’t like, like out of those first 10, choose the one you don’t like.
The most and do 10 variations of that until you make it look like something you like because that’s the thing. There’s there’s no I don’t think there’s any wrong way to animate something, but there is definitely Nicer ways to animate something in a way. You’re not supposed to animate it because it’s not wrong if people like it
It’s like you can do something completely Not what is expected as long as it is pleasant
## Emil (55:16)
Yeah, I guess that’s why animations are so hard for some people because there is like with code when you make something wrong There’s like a clear error. Something doesn’t work and with animations, you know, they’re obviously better and worse animations, but it’s not black and white As Michael White has told us, you know.
## Lochie (55:33)
Yeah, with animation I feel like a lot of the time you just need to iterate as many times as you can until you find your style or your preference or, like, a lot of people what’s that bell curve called? The Dunning Kruger? Is that the one? Yeah. A lot of people, they don’t have any knowledge and they think it’s easy and they have some knowledge and they think it’s super difficult.
And then they have, or they know they have some knowledge and I think it’s super easy even then, and then you get to the other side and you’re like, Oh, I know how to do all this stuff, but I still don’t know how to do all this other stuff. And it’s like you see it a lot with like people who do PowerPoint presentations, but they add in animations and it’s all crazy.
And it’s like, there are things coming in from the left and the right. It’s like all over the place. And it’s really hard to pay attention to what they’re talking about. When there’s so much stuff going on behind them, but then you see really good presentations where there is animations and transitions between different pages, but they’re all subtle and quick and like, they’re not in the way.
And they’re just kind of like they’re adding to the conversation rather than becoming the focus because it needs to compliment what the content is rather than take control of what people are paying attention to, right?
## Emil (56:50)
Yeah. So I guess what I’ve been hearing a couple of times during this interview indirectly is that, you know, a collection of references is very useful.
Like you are building your collection of references all the time by, you know, looking at tweet tweets and insights that you like and whatever. I guess that’s a very useful thing for people to have a collection of references and pull from those references whenever you need to make an animation.
## Lochie (57:18)
Definitely. I think that everything. Everything is a remix of something else. Like you’re always like everything I make is probably referencing something else that I saw years ago or, and all I’m doing is replicating a lot of the times, but I’m also replicating something that is joined with a bunch of other things that I’ve also like looked at before.
So, and then all that kind of melds into my own style. It’s the same thing like if you learn how to draw, the first thing you do is you draw a little stick figure and then eventually you go, okay, I want to draw something, a face. So you think, how do I draw a face? And you reference like either some people, like they’ll try to draw an actual face or they’ll start referencing like the cartoons that they watch or something.
And then you’ll draw like the little, like two dots and a smile, like adventure time style or something. And then from there people go like, Oh, what if I changed the ears to the ears of a different show that I’ve seen or. a different thing and just all they’re doing is just like creating a different character based on all the things they’ve consumed and all the things they enjoy and all the things they like to look at and then eventually they get something unique and something that’s not been done before or something that’s has its own style and its own visual language and I think the same thing works with.
With animation or even design or any, anything really just like take everything that you like and take inspiration from everywhere and kind of bring it together and create something that is uniquely yours and something that has its own style and just by just keep gathering references and gathering ideas and just keep building, keep making stuff.
## Emil (58:59)
Yeah, I think that’s a great way to to end this interview yeah, is there anything else you would want to like share or you know promote right now?
## Lochie (59:09)
I don’t know not too much. I’m i’m always on twitter. I’m i’m always lurking but i’m on twitter at lucky axon I have a few different projects i’m working on that.
I’ll be sharing on there For the most part. I just want to see what other people make. I just want to see I just want to see people do cool things. That’s, that’s what I’m most interested in. Yeah, just so if everyone can make cool things, that’d be great. Share them with me. I would love to see them.
Cool.
## Emil (59:42)
All right.
## Lochie (59:42)
Yeah, thank you, Lucky. And thank you. I, I, it’s really nice to, to, well, I’m very grateful to be on this chat. So, yeah. And it’s really cool to see all the cool things that are coming out of this, this course as well. It’s really cool. Thank you so much.
## Interview notes
- Lochie’s Twitter
- Family
- easing.dev
- Avara
---
# Interviews/Mariana Castilho
- Module: Interviews
- Lesson URL: /lessons/interviews/mariana-castilho/
- Markdown URL: /md/lessons/interviews/mariana-castilho.md
- Source mirror path: apps/anim/learn/interviews/mariana-castilho.html
Mariana is a Brazilian product designer currently working at pooolside. She created a project called uilabs.dev where she shares her UI explorations. In this interview we are going to talk about product design, how she learned to code as a designer, and how that influenced her design proces
## Overview
- Henry Heffernan
- Mariana Castilho
- Lochie Axon
- Dennis Brotzky
Mariana is a Brazilian product designer currently working at pooolside. She created a project called uilabs.dev where she shares her UI explorations. In this interview we are going to talk about product design, how she learned to code as a designer, and how that influenced her design proces
## Emil (00:19)
Hi everyone. In today’s interview, we are talking to Mariana, who is a product designer at Vercel at the moment. I’m a big fan of her work.
Hi Mariana, can you tell us a bit more about yourself to start?
## Mariana (00:41)
Yeah, sure Emil, I’m super happy to be here.
I’m Mariana - I am a Brazilian product designer. I’m currently based in Brazil. I work as a senior product designer at Vercel in projects related to AI, like AI integrations, AI studio. Also marketplace and marketplace integrations. And some of the collaboration projects as well.
## Emil (01:07)
What did you do before you joined Vercel?
## Mariana (01:11)
Before Vercel, I was working at the Universe, which is an iOS app, basically an easy way to build and publish websites and small commerce.
So there I was leading the beginning business and platform, all the product design work related to business and platform. And then I led all the work related to translating all the iOS interfaces and features that we had for web. I’ve been freelancing for a while as well.
## Emil (01:46)
Did you learn design yourself or did you go to school for it or how did you start?
## Mariana (01:56)
That’s a great question because I did advertising marketing in school actually, but I also studied design at the same time, so I was trying to pursue two degrees at the same time.
And at some point, it was just a lot. I started to do some internships and stuff. So yeah, it was really a lot to deal with.
Eventually I think my parents at the time, they didn’t really know what really design was. So they said, no - just finish marketing and then figure it out.
So when it comes to design, I feel like my relationship started very early, really when I was a kid. I was very into Dragon Ball and all these cartoons at the time. I grew up on internet forums and small websites.
I always was really into web design, video games, art. So I kind of like my relationship with design started pretty early. But when it comes to interface design, software design - basically I learned alone really. Studying, working, freelancing. I didn’t have an official design degree.
## Emil (03:09)
Interesting, so I’m guessing a lot of people listening are also interested how you got your job at Vercel. How did that happen? Did you apply or did someone reach out?
## Mariana (03:28)
Yeah, so I was always really into publicly sharing my work and explorations that I create in my free time. And I usually share this stuff on X or Twitter or whatever you want to cal it. So yeah, I’d been sharing some work there. And at the time, Guillermo of Vercel CEO saw my work. And yeah, basically he mailed me asking if I was interested in chatting about joining Vercel.
I got super thrilled because I was and I still am a huge fan of Vercel Design Team - I was super happy when he contacted me. Long story short, here I am. But basically Guillermo contacted me after seeing my work on Twitter.
## Emil (04:10)
That’s basically what Henry said and what happened to me as well. Yeah, sharing your work is the best thing you can do in this time.
And I guess you had an interview process, right? Like a normal one, but that helped you get noticed?
## Mariana (04:36)
Yeah, I talked to some people and I interviewed. But for sure, just the fact that he noted my work helped me a lot instead of just applying like everyone else. For sure it gives an advantage, right? Because you’re already noted basically.
That’s why I really recommend people sharing their work on social media or like X or other platforms. A lot of like my contacts and people that I’ve known that are great came from Twitter, I would say. So building your network and building contacts opens a lot of doors for also freelancing or full time jobs. This is something that I always recommend and forever recommend because it’s really changed my career. My job at Vercel really changed my career. I’m really grateful for all of that.
## Emil (05:37)
Yeah, I fully agree. You mentioned, you talked to a few people during the interview process. Did you have to do a design exercise or something? Or was it just talking to people?
## Mariana (05:53)
Yeah, that time was more talking to people, talking about my experience, my projects. I think I talked to three or four people at the time. Basically sharing the work that I’m sharing on Twitter - so I felt that people are already kind of familiar with my work and stuff.
It was more about like chats, really.
## Emil (06:16)
Okay.
I personally really like your taste in design and, as someone who is not a designer, I’m wondering - like you said you did a lot of stuff - is that the most important part of becoming a designer? What would you say was the biggest accelerator in your career outside of sharing your work, which you just said?
## Mariana (06:56)
Great question. Really looking back to my career, I think there are really two key factors that helped me a lot to get where I am.
The first one, which I always mention, is having side projects and dedicating time for free explorations outside your work. And obviously, as we mentioned, even better if you can share them publicly. This was and still is key to my process and to really polish and exercise my craft. Mainly because it lets me explore different techniques and different ideas than what I would usually explore in my daily work. And given the time, I need to exercise my craft and my taste, so that’s key. It’s something that I continue to do. That requires great time management. I know sometimes that it’s not easy and sometimes it’s not possible because sometimes just have a lot going on. And there are multiple periods that just have a lot of things in my life in general. And it was just not possible to keep up with the time and energy that things require.
But I think that’s also important and okay to know your limits and allow yourself really to rest and recharge. But yea - getting better and better with this time management and planning how much energy I can invest helped me a lot.
So, for sure, the first thing is side projects or free explorations. Giving yourself some time away from your work to polish your craft really.
The second thing that helps me and helps me still is that I’m really passionate about what I do and I do really have an ambition of creating extraordinary things. And this is more like an aspirational goal for sure. There is no such thing as perfection, but I always try to improve and I’m always trying to get better and better at what I do.
I just love to design and build stuff. So it rarely feels like I’m actually working and it’s actually like an obligation. It just feels natural and something that I want to do. So yeah - this helps me a lot maintaining my motivation high.
I think these are the main two things that’s made and still make a huge difference in really upscaling my career.
## Emil (09:38)
Yeah, that helps me with my motivation as well, to trying to get better and better and creating something very, very nice at some point. Something that I can be really proud of. And I know that every day that I do this type of stuff, it helps me to get closer to that.
## Mariana (09:59)
Yeah, and it’s crazy. Like sometimes I revisit some past work from months ago and I completely see total change sometimes in my style and the techniques that I’m using. And I feel like I created this work 10 years ago, but it was really six months ago. So yeah, it’s these daily doses of effort and time and energy that’s built up into really changing how we approach design.
## Emil (10:31)
Yeah, I really like that feeling - like looking back at thinking that you made something like you, it’s not what you would do today, or like you would do something, make something better today because that’s like shows that you are actually getting better. But yeah, that’s really cool. And again, I’m not a designer and I’m really curious how designers think.
What sets apart a great product designer from a good one? What’s the distinction there? In your opinion, obviously, because I guess it’s subjective. But if you look at a designer in general, what do you notice and what makes you feel that he or she is very, very amazing?
## Mariana (11:23)
Yeah, that’s very subjective and that’s my personal take. Obviously that’s not a rule, but that’s how I feel about it.
To me, what I try to observe is that the designer in question is nailing both form and function. So in a nutshell: function is really like how you can work on the problem space and break down really complex problems and reparations into like simple interfaces and solutions that are really relevant to that problem. So nailing this part of the function is really important because product design/software design, they need to have a purpose and they need to have an intention behind it. It’s not only about looking great. So that’s the first part. If the problem space is being well-defined and if the problem questions being solved in an elegant, simple, and intuitive way.
The second part is the form part, which is really related to taste, to really differentiate what looks and feels great from what doesn’t. And craft to know how to execute with excellence, like your vision based on your taste, which is also part of this equation.
Often I see some things online and tweets where I see that the idea is really good and what’s behind the idea is really good, but the execution is just not quite there yet because either it lacks like taste or because the technique’s not quite there yet.
That’s my personal opinion, of course, but I feel like a great product designer has a great balance between nailing the idea and the problem solving part, but also having the taste and the technique to create something that’s amazing.
## Emil (13:15)
And what would you say is like harder to do? The taste part or the idea part?
## Mariana (13:22)
I think it really depends on your background and your skills really. I don’t know, that’s a great question. I would say that it depends on the project as well. I feel like there are some projects that is much easier to nail the problem space and to come up with a great idea and a great foundational UI and it’s hard to make these UI really interesting, appealing, and delightful to use.
Some other projects are really the opposite - like the visual idea and how it should feel is kind of clear in your mind, but you’re struggling to link that with the actual function and how it should actually solve the problem. I think these two things should work in parallel, really.
I don’t have a good answer for this question. I think it depends.
## Emil (14:20)
No, I think that’s a great answer. So I think we can move to another part.
I don’t know design but you obviously started UI labs where you share your things you built with code. Obviously a lot of people like this project , and I like this project as well and the first question is why did you decide to start learning code?
## Mariana (15:04)
Yeah, that’s a great question. And I’m happy to see that a lot of people like the project. It started not really pretentious. I didn’t want to achieve nothing specific just to share some explorations. And yeah, people seem to like it. So I’m really happy about it.
I think my motivation to learn code and to go deeper into UI engineering came from a really strong desire to know how to build the work that I was creating. And I think a lot of these other designers can relate to that, but a lot of times when you finish your high fidelity designs and you have like your Figma file and everything looks great and you’re feeling happy about it, and you hand over to the engineer and hope they will be the right; even though they built it completely right and it’s like one-to-one with your Figma file, everything feels perfect, like spacing, textiles, color styles, everything’s like exactly as it should be; sometimes just doesn’t translate right.
It’s more of a sensation and less something like objectively wrong, if that makes sense. Like there’s something missing and you cannot tell exactly what it is because on the paper everything is matching. And I think this frustration of not knowing what was missing grew into a place that I just wanted to be autonomous to build my own work and really to go on the deepest level of understanding: how I can build it and really try to find the secret sauce. That was really the motivation that made me go deeper into UI engineering and eventually build UI Labs because, as I mentioned before, side projects and explorations are my success formula to learn something. So I just applied the same formula with code. I just started to work on side projects and tried to do stuff myself. And eventually this little project came out.
## Emil (17:06)
That’s really cool. You said you started doing that because you wanted to build the designs or try to learn it, but does that also mean that you work in code at Vercel as well or is that not the case?
## Mariana (17:26)
In the beginning that was not the case. And that’s why exactly why my side projects were super important because - I will speak from my own experience - when I was starting to learn code and UI engineering, I was not feeling confident on my skills and I was not feeling confident that I was making the right decisions and I had the right techniques to solve some specific engineering problems. So when I started to work on UI labs and some other like small projects on the side during my free time, I started to feel like more and more confident that I knew how to solve like really a lot of different problems that I didn’t knew like some months ago. So in the beginning, I was not working code at Vercel and at some point it’s built into a moment that I felt confident about it. So just like drop some lines here and there. I pushed some PRs and I learned as well from these PRs. So today - yes, it’s not my primary function at Vercel. I’m primarily designing. But when I have some time, I love to jump into code and just like try to fix things here and there, build a component myself.
## Emil (18:34)
Yeah, that’s really cool. It’s really, really powerful as well. You can just do everything yourself.Yeah, that’s really cool. It’s really, really powerful as well. You can just do everything yourself.
## Mariana (18:42)
Yeah, and also when you communicate with engineers it’s kind of different because you will learn how to speak the same language. Mommunication is just so much easier than if you didn’t like know how to code. A lot of things really improved since I went into UI engineering really.
## Emil (19:01)
And how did you learn it specifically? Because engineering in general is pretty big right? And can be quite overwhelming at the start. Did you read a lot of articles, bought a course that helped you?
## Mariana (19:28)
Honestly, just sitting down and studying, getting access to some great content like Josh Comeau’s course really helped me on React, CSS. Sam Selikoff’s Build UI, also super nice, and your course as well, Animations on the Web, you know that I’m a fan of it. For the animation part, it helped me a lot. There’s so many great content outside. They’re from great creators, really great people. They’re willing to help. Basically I just studied alone, watched some courses. The key part to learn and to really solidify all the knowledge was starting to build stuff and make mistakes and research and learn how to solve these mistakes.
## Emil (20:19)
How long did it take to get you to where you are today in terms of coding skills? What we can see on UILabs, for example.
## Mariana (20:34)
I feel like I’m still learning really. Like I don’t feel like I’ll ever stop learning. But I started UILabs in February, I guess.
Even on UILabs, I see my first components there and the latest ones and I already see a huge difference in terms of technique/quality. So I really see myself evolving also on the engineering side. I don’t know really, I don’t know how to precise the time that it took because everyday is a new day that I want to learn something new so I’m not really doing any math in my head on how much time I need to arrive on that specific point.
I also don’t want to send a message like, “hey you should do that for this amount of time”. You learn it because people are different and these formulas never really work.
## Emil (21:35)
Yeah, sure. Was there like anything that surprised you about coding? Because like you were designing for a long time and you kind of knew and you were working with engineers for a long time.
Was there something that made you say “wow, I didn’t expect it to work this way” or something like that. I’m just really curious about it.
## Mariana (21:56)
The most surprising thing to me that I was not really expecting or I didn’t foresee was really how learning to code would influence my designs and my work as a product designer. So kind of like the other way around really.
Learning how to code completely changed the way I design. I feel that now I design it with much more intention because I know exactly how the final output can be built. So while I’m designing, while I’m in Figma, for instance, I’m all the time doing like this relationship with code and seeing, “okay, now here I can apply Flexbox, I can set a padding of like four pixels” or whatever. I’m all the time doing this math and this really makes my design much more intentional, really. I’m also much more critic about like polish, how the state should transition and how the experience overall should feel. In general I feel like the surprising thing to me was how this changed who I am as a designer really and my work as a designer.
## Emil (23:07)
Right, that was kind of my next question, but you answer it already.
Also from an engineering perspective, you can sort of see who is a designer that knows how to code based on the Figma designs that you see, which is also very interesting. It confirms the thing you just said, that you think about Flexbox and everything, how you could do it.
Another UI labs-related question. Do you design it in Figma or do you jump straight in the code?
## Mariana (23:53)
That’s a great question and some people ask me this question on Twitter.
Most of the times I start sketching in Figma just because it helps me exploring different ideas faster than I would take in code. In Figma, I feel free to just create multiple frames and go crazy and see what sticks really. But once the general idea is locked in and I know exactly what I want to build. Most of the times I don’t even finish my designs in Figma, I just drop Figma and I move directly into code, and I start creating the UI there. 99% of the times, the final output in code and what goes into production looks nothing like my initial sketch in Figma.
When I start to code, the direction usually changes or I realize that there are some certain constraints or behaviors that I didn’t anticipate when I was designing Figma. I create an improvisation of code to get to the final outputs that I share on UI labs and social media. That’s really the workflow that I found that works best to me.
About Figma prototypes and stuff, I never use it really. I use it more for flows, for instance. If I want to explain how a flow should behave - like transition between screens. For this specific goal, I use Figma prototype. But apart from that, to create animations for component animations and stuff, never. Everything into code.
## Emil (25:34)
Okay, so you just like wire the Figma designs up so that you can click on the button and go to a different page basically.
## Mariana (25:42)
Yeah, usually when I need to explain to some other people what I’m thinking about a specific flow, yes. But that’s kind of it.
For more polishing animations. it’s just easier to do directly into code because you have more granular control over the parameters than using Figma prototype. It’s just easier to do directly into code.
## Emil (26:09)
Yeah, that makes sense. When you work on a side project or at Vercel - let’s say you’re stuck on a problem or on a design task in general, where do you get your inspiration from? Are there any people you look up to? Any books, art? How do you handle being stuck?
## Mariana (26:38)
Sometimes I just can’t stand it, it never goes away and I stay blocked for like days in a row which sucks. Usually, inspiration comes from really random places like art. I’m really into art - I love going to museums and I love trying to rationalize what makes great art great, really, so that’s something that I really enjoy. But also video games, I’m super into video games, nature, movies, all this cliche.
For sure there are some people and some teams that I look up to. I really like the Vercel design team. It’s an amazing group of product designers and product engineers. They inspire me a lot. A lot of times when I’m stuck into some problem on Vercel or there are some things that I don’t know exactly how to design or I’m having like a creative block - I just talk to some people from the team and I see their work and they usually really inspire me a lot. There’s also Family team that has a super incredible level of consistency, shipping, excellent stuff. The Linear team. I feel like we’re fortunate to live in a era that we have a lot of great talents around us. I really look up to different people from different teams. I think they’re all amazing.
## Emil (28:01)
You mentioned thinking about art and what makes art great. And I have a related question. But how do you think about taste and how can someone develop if someone can develop taste at all in your opinion? Taste when it comes to design specifically, and how would you get better at that in your opinion.
## Mariana (28:35)
Well, I think this question deserves an entire episode. That’s again my personal take, not claiming to be the source of truth, but I do feel like taste can be developed. I do feel like you can also develop taste in general and not only taste for design.
The first thing is to define what’s taste, right? And to me, taste is really the ability to recognize some patterns and systems that feel extraordinary and feels that have a high aesthetic value. So that’s for me what’s taste. It’s also kind of subjective, but that’s how I try to rationalize it. These patterns and systems can be composed by repetition, balance, symmetry, all these general rules that we also see in design.
To me, the best way to exercise taste is being exposed to great stuff. Being exposed to great art, to nature, to great design, to great music, and be really intentional and refine your intuition about why something has a high aesthetic value and try to translate that into all these characteristics that I mentioned, like balance, symmetry.
If I’m seeing a painting and I feel like “wow, that’s incredible, that’s extraordinary”, and I feel like there is a lot of great taste into that - I just try to rationalize. “Okay, why do I feel this way? Is it about the balance, is it about the composition, is it about repetition, is it about the color scheme?”.
It’s the same thing for design. That’s what I’m doing all the time when I see work online or like magazines, or some sort of design work that’s extraordinary. I just try to understand why this touches me in my heart and why I feel that’s extraordinary. Sometimes I come with “I feel like it’s because it’s balanced like these” or “the colors are really pleasing” or something else. Or sometimes I just cannot tell and it remains a mystery to myself, but I keep thinking about it.
I think people should talk more about taste because at the same time that feels super subjective. I do feel that it can be developed and can be rationalized and people can put more intention into trying to develop their own tastes.
This is a very extensive topic and probably I’m not the most skilled person to talk about it, but it’s something that’s on my mind all the time when I’m working.
## Emil (31:17)
Yeah, okay, that’s very interesting. So if you browse the web, let’s say, or even on Twitter, you also pay attention to the things you see. And if you find something you think that’s very beautiful, like even a website or a tweet and animation, you try to think why you think it’s beautiful.
## Mariana (31:37)
Yes, yes. It’s kind of like - as my side projects, which are something that I try to keep as my everyday practice, I try to do this as well.
When I’m like browsing the web, or reading some books or magazines - the medium doesn’t really matter it’s more about the intention behind it - I try to ask myself this question.
Sometimes it’s not even something that struck me as something beautiful, but something that touched me or caught my attention for some reason. I try to just rationalize why this specific thing caught my attention among the sea of other design work being put on Twitter, because there’s a lot of work being put on there. A lot of great work, we have a lot of great creators.
When something really caught my attention in a special way, I really spend some time just meditating around that and trying to understand what makes it so unique and so extraordinary.
## Emil (32:39)
Yeah, that’s very interesting. I do that as well. I scroll on Twitter a lot, and it’s kind of useless but I also think that it helps me refine my taste. I guess there are better ways to do that, or more productive ways, but I don’t.
## Mariana (32:59)
I think it’s fair. I do that a lot as well. Even to see things that sometimes I acknowledge that there’s taste behind and there’s clear intention, but that’s not something that’s aligned with my own taste.
I think that there is also this thing about perceiving taste and intention, but also nurturing your own. There’s a lot of work that I see where it’s polished, there is a clear intention and there is taste behind it, but it’s not really aligned with my own taste and what I’m nurturing for myself. But I also think it’s interesting to really build and create this distinction - find your own style, which sometimes is hard.
## Emil (33:39)
Yeah, that makes a lot of sense. We are close to the end, but something you published - last week, actually, at the time of this recording - is an announcement that you are going to make a course together with Derek from Clark.
Why did you decide to do it and why this type of course, sort of an engineering course for designers, right?
## Mariana (34:27)
Wow, this is a philosophical question.
I am creating this course with Derek, who is amazing. My motivation was really to build something that I would have liked to see when I was starting to learn how to code and really making UI engineering more approachable for designers.
When I started to work some courses and stuff that were directed to engineers, I felt lost in the beginning and it took me some time to learn how to start because there’s like so many slangs and so many different ways of approaching those specific problems that I was not used to because my mindset was like a designer mindset.
So our ambition is really try to create something that talks directly to designers and to the designer mindset, and helps build this bridge. These are some cool techniques to come to create your own components, but that’s how you like migrate these into a UI engineer mindset with different concerns and different things to think about.
Just trying to create something that I would have liked to see. That’s the key motivation behind it.
## Emil (35:52)
Yeah, that’s really amazing. I’m very interested in the course.
Will there also be a design part of the course where you actually design the stuff that you’re going to build or is it code-only basically?
## Mariana (36:14)
Yeah, it’s going to be a lot of design parts.
This is still being decided, but we also had a lot of great response from engineering, wanting to learn a little bit more about design, which is super interesting.
TL;DR still defining the structure of the chorus. We’re working on that. But the idea is that we will have parts where we will design some components from scratch, trying to teach some cool techniques. And then we’ll take these designs and build also from scratch. So basically you learn how to design and to build the components and the interfaces from scratch. That’s the idea.
## Emil (36:56)
Nice. That’s amazing. It’s an insta purchase for me, so I can’t wait when it’s going to be released.
Let’s go back to for the last question: what advice would you give someone outside of the advice you’ve given already in this interview for someone who is looking to improve their design skills, whether it’s an engineer that wants to get into design or someone that just wants to become a designer? Are there any books courses or anything else that you would do if you had to learn it again?
## Mariana (37:38)
I feel like I’ve said it all, honestly. And these are things that I continue to do: projects, explorations, really put intention in your free time and of course, as much as you can, respecting your own boundaries and your own mental health and energy. Put some intention in your free time to exercise something that you want to learn either if it can be design or engineering.
About courses - I’m a huge fan of your course, Animations on the Web. I’m also a huge fan of Sam Selikoff, Josh Coumeau. These courses help me a lot.
A lot of people send me messages asking like teach me how you learn, teach me how you got there. But honestly, there’s no secret formula really. It’s more about discipline, time management, and putting the right intention behind things.
I know it’s hard. It’s much easier said than done and I know that I’m saying this now and it sounds super easy but it’s actually not and it’s hard to keep motivation and to keep the energy week after week, month after month and not letting your intention drop but there’s no shortcut. You just need to put some time and learn it really.
And the more you do, the better you get. And from my experience for both design and engineering, the more I design, the more I get better at design and the more I code, the more I get better at engineering. And yeah, there’s really no shortcut. And that’s how I learned at least. So that’s really my advice.
## Emil (39:26)
Okay, and when it comes to the discipline you mentioned, do you have have strict rules? Do you work at least a certain amount of time when you start projects, or is it just doing it every day and that’s it?
## Mariana (39:40)
I don’t have any strict rule. Overall I am someone very disciplined in all my life. I’ve always been like this, so it comes kind of naturally to me to commit to something and to stick to it.
But something that I’ve learned is actually how to really respect my mental space and my energy as well. Like we know each other and you know that I’m really passionate about design and engineering and like building stuff and rarely it feels like I’m working. It’s really something that I’m passionate about. And a lot of times I just felt on my body and in my mind that I was just giving it too much. I was just putting too much time and too much energy. I was like not paying attention to other stuff that are also important to me and respecting my time to recharge and rest.
So I kind of learned the opposite. I kind of learned how to let myself recharge, let myself really do other stuff, go do some sports or hang out with my friends and read a book and really respect my limits, which exist and I’m very human. I feel like the discipline part for me was the easiest one. It took me some time to learn on how to give my body and my mind the rest that they need. I used to feel guilty about it to be honest, like in days I was not really feeling like working on my side projects and stuff, I was feeling guilty and I felt like “I’m wasting time”, but that’s absolutely opposite. When I take a few days of a break to just do something else, I come in much more recharged and much more energy and creativity to work all my side projects, or even at my work.
My last advice would be to go full in, but also respect your body and your mental health. It’s really, really important. If you go all in, but your mind’s not good and your body’s not good, your health’s not good, nothing of this really matters at the end. Just prioritize your health, your mental health and try to get as much discipline as you can.
## Emil (42:06)
Yeah, that’s really good that you said that. You said that you felt guilty before about not doing something, and seeing people posting great stuff on Twitter or somewhere else also makes you even more guilty of not doing something because you see all those people post amazing things. But oftentimes it’s just an illusion, because I don’t do stuff all the time, but I do post on Twitter the stuff that I make.
So it can create an impression as if you or me are working 24/7 on it, but that’s obviously not the case. And I also feel guilty sometimes.
## Mariana (42:49)
It’s also learning about how to deal with our own frustrations of seeing great work - like “oh my god, I wanted to create something like this and what am I doing with my time, I should be working and polishing my craft” - which I think is a great mindset and it’s my mindset - but honestly, for real, at the end of the day, if you’re not healthy, if our mental health is not good, it’s useless because at some point you break at some points our body tells us to stop and just put attention on recharging and resting and taking care of your relationships, of your life outside work, which to me is really important.
This is something that I would say that I’m still learning how to deal with that, with guilt and feeling that I should be working more. But today I do really appreciate all the time that I’m not working and I’m doing something else because there’s also a lot of other stuff. They’re super important to me and I feel great to be able to take care of them as well.
## Emil (44:01)
Yeah, that’s very good for you. And I agree with everything you said.
Those are all the questions I had for you. It was great to talk to you. Is there anything else you would like to talk about in this interview?
## Mariana (44:21)
No, not really.
I came in feeling that it would be more like an interview, but we’ve known each other, so it just feels like a normal chat.
I feel like I’ve shared everything that I want to share. Thanks everyone for listening and for going on UI Labs and giving great feedback. I appreciate it.
Keeping the learning mindset, keeping spirit high - that’s really it.
## Emil (44:51)
Thank you for your time. I’m waiting and others are also waiting for the course. I will link it somewhere down below. Thank you.
## Mariana (44:59)
Yes, I’m excited. Thank you.
## Interview notes
- uilabs
- UI Engineering for Designers
- buildui by Sam Selikoff
- Josh Comeau’s courses
---
# Animating the menu
- Module: Navigation Menu
- Lesson URL: /lessons/navigation-menu/animating-the-menu/
- Markdown URL: /md/lessons/navigation-menu/animating-the-menu.md
- Source mirror path: apps/anim/learn/navigation-menu/animating-the-menu.html
Let’s try and build our menu. This is how our menu will look like after we’re done with the exercises:
## Overview
Let’s try and build our menu. This is how our menu will look like after we’re done with the exercises:
This lesson is split into multiple exercises to reduce complexity. Before we dive into them, let’s go through the starting code first.
## Starting code
Radix’s primitives are compound components, which means that each component is actually a group of smaller components that all work together to perform a task. Radix has an anatomy section for each component, which shows how the group of components is structured.
Each NavigationMenu.Item is a container for NavigationMenu.Trigger that when hovered opens NavigationMenu.Content. NavigationMenu.Viewport is the element in which the content is rendered. I also created a component to not repeat the same code for each list item.
This code is pretty straightforward once you understand what each component is responsible for. You can also read the API reference to understand it better.
There are also some styles already applied to the menu. I’ll not cover them in details as I would prefer to focus on the animations, but feel free to read them if you want to understand how I styled it.
## Width animation
The first exercise is about animating the width of the menu as its content changes. I don’t give you a lot of hints on purpose, as all the answers can be found in Radix’s Docs. The end result should look like this:
This is partially an exercise about how to use an external library and use docs correctly. If you are stuck, you can use the hint below. Also, you are free to choose your own easing and duration for all animations, but think about the principles you learned in the first module. I’ll explain my choices as well.
```text
import * as NavigationMenu from "@radix-ui/react-navigation-menu";
import "./styles.css";
export default function Orchestration() {
return (
Interviews
Product designer at poolside, previously Vercel.
A design engineer at Vercel working on v0.
Co-founder of Fey, engineer who loves design.
Lessons
Consists of 4 modules, 24 lessons and 50+ exercises.
Interviews with experts from companies like Vercel.
Join an exclusive community on all things animation.
Consists of 3 walkthroughs of high-quality components.
A highly curated list of animation resources.
Get your certificate of completion after finishing 70% of the
course.
);
}
```
## Enter and exit animations
Let’s now tackle the enter and exit transition when we hover over a trigger. The end result should look like this:
```text
import * as NavigationMenu from "@radix-ui/react-navigation-menu";
import "./styles.css";
export default function Orchestration() {
return (
Interviews
Product designer at poolside, previously Vercel.
A design engineer at Vercel working on v0.
Co-founder of Fey, engineer who loves design.
Lessons
Consists of 4 modules, 24 lessons and 50+ exercises.
Interviews with experts from companies like Vercel.
Join an exclusive community on all things animation.
Consists of 3 walkthroughs of high-quality components.
A highly curated list of animation resources.
Get your certificate of completion after finishing 70% of the
course.
);
}
```
## Switching between menus
The last animation we’ll add is the open that happens when you hover over another trigger while the menu is open. The old content fades out while the new one fades in from the other side. This is how the end result should look like:
Other than the animation itself, there’s one detail to it. We’ll cover it in the solution, but if you are curious, play with the demo above and see whether you can figure it out.
```text
import * as NavigationMenu from "@radix-ui/react-navigation-menu";
import "./styles.css";
export default function Orchestration() {
return (
Interviews
Product designer at poolside, previously Vercel.
A design engineer at Vercel working on v0.
Co-founder of Fey, engineer who loves design.
Lessons
Consists of 4 modules, 24 lessons and 50+ exercises.
Interviews with experts from companies like Vercel.
Join an exclusive community on all things animation.
Consists of 3 walkthroughs of high-quality components.
A highly curated list of animation resources.
Get your certificate of completion after finishing 70% of the
course.
);
}
```
## Why use a library for this?
Most of the code in the exercises above is copied from Radix’s documentation. You might think, "Isn’t this course supposed to teach me how to build things from scratch?". Yes, sometimes.
This walkthrough is not so much about teaching you the technical details of implementing Radix’s navigation menu, but rather about showing you how I would approach building such component in a real world scenario. I’ve done this at Vercel and Linear, and I know that I would built it in the same way now.
Your users won’t see your code. They won’t see the difference between you spending 20 hours on a component vs using an existing solution, as long as an existing solution is of high quality. When I know that using an existing component will save me time, make the code more readble and maintanable, and leave me with a better end result, I’ll use it. And I think you should do the same.
---
# Closing thoughts
- Module: Navigation Menu
- Lesson URL: /lessons/navigation-menu/closing-thoughts/
- Markdown URL: /md/lessons/navigation-menu/closing-thoughts.md
- Source mirror path: apps/anim/learn/navigation-menu/closing-thoughts.html
Our menu is basically finished at this point. In this lesson I’ll discuss a small improvement we can make and we’ll talk about choosing libraries.
## Overview
Our menu is basically finished at this point. In this lesson I’ll discuss a small improvement we can make and we’ll talk about choosing libraries.
## Reduced motion
Remember when we talked about accessibility? We talked about how animations can make people feel sick. This is one of those animations, because there’s a lot happening at once.
To disable motion for people that prefer reduced motion we need to remove animations within a media query:
```css
@media (prefers-reduced-motion: reduce) {
.viewport,
.content {
animation: none !important;
}
}
```
```css
@media (prefers-reduced-motion: reduce) {
.viewport,
.content {
animation: none !important;
}
}
```
This is a pretty quick fix, but you should ask this question yourself with each animation. Should we disable animations for people that prefer reduced motion? Don’t forget it, people appreciate it!
## Should you still use Radix?
Radix is not as maintained as it used to be, and there are great alternatives these days like React Aria or Base UI. While Base UI is the shiny, new thing, Radix is mature and battle-tested. Also, code doesn’t stop working just because it’s not maintained.
Again, you should carefully consider all the options and choose the one that makes the most sense for you. For me, it’s Radix at the moment. I haven’t ran into any big issues with it, and in the worst case, you can always patch an issue or fork the library.
The good news here is that Radix and Base UI implementations are very similar. If you want to refactor this component to use Base UI, you’ll only have to change a few CSS variables and data attributes. Less than 5 minutes of work.
## Trusting the hype
When I was starting out Stitches was the hot new thing. Everyone thought it’s the future of styling so I decided to use it at a company I was working at. It was great... for a few months. At some point the Stitches team has moved on to other projects and it wasn’t maintained anymore.
I made a mistake. I wanted to use the new shiny thing, because people in the community were impressed by it at that moment.
Don’t chase the new shiny thing like I did. Think about the pros and cons and carefully consider all the options.
Maybe Base UI will be the best thing that happened to the web, maybe it will be unmaintained in a year. I don’t know that. What I do know is that Radix solves a problem that I have and it won’t break because of lack of commits.
To be clear, I am aware of Radix’s issues. I just don’t consider them big enough to make me switch to Base UI at this moment. With that being said, I want Base UI to succeed. The web deserves great primitives.
---
# Know your tools
- Module: Navigation Menu
- Lesson URL: /lessons/navigation-menu/know-your-tools/
- Markdown URL: /md/lessons/navigation-menu/know-your-tools.md
- Source mirror path: apps/anim/learn/navigation-menu/know-your-tools.html
Let’s assume we work on the marketing team at a company and we are tasked with creating a navigation menu with multiple dropdowns. How would should we go about this?
## Overview
Let’s assume we work on the marketing team at a company and we are tasked with creating a navigation menu with multiple dropdowns. How would should we go about this?
What I usually do in such cases, is go and look how other companies do it. I actually created a navigation menu before, and what I did before I built it is I looked at how other companies do it and decided to build something similar to Stripe’s.
This was my inspiration for Vercel’s navigation menu.
This is how I usually work. Nothing is done in isolation, I get inspired by other products. Most of the time, you shouldn’t try to invent a new patterns, but rather use existing ones, because they are battle tested and proven to work.
This is especially useful early-on in your animation journey when you don’t have the intuition yet. Look at great products and companies, see how they do things, and then try to create something inspired by that. Steal like an artist.
## Knowing what to use
A menu like this is actually quite a complex component. Keyboard navigation, accessibility, correct focus management, are all things that we need to get right. While we could do all of this from scratch, we can also look at existing solutions like Radix’s primitives or Base UI.
Making a dropdown accessible is actually quite challenging, same goes for a custom select component or a toast. Components like Radix’s primitives offer all that accessibility out of the box, but at the same time, they are unstyled which gives us a lot of freedom. These libraries essentially do the "boring" work for us.
When I was working on Vercel’s design system I used Radix’s primitives as the base for lots of components, because it saved me a lot of time, but also because the developer experience of these components is great. I also used it recently for Linear’s navigation:
Linear’s navigation menu.
Radix also happens to have a navigation menu component, which is exactly what we need. This is the importance of knowing your tools. Don’t blindly jump into code straight away. Look around, see what’s out there, what libraries or components could make your life easier.
Radix makes animating the navigation menu easy as well, which is another reason why it makes sense to use it. It exposes data attributes like data-motion which will even allow us to ensure that the animation is direction aware.
## When to use a 3rd party dependency
You should obviously carefully consider whether a 3rd party dependency is worth it. But for something like a navigation menu, you have to consider that it would take you a lot of time to get this right. Someone has spent weeks working on such component already. Not only will it save you time, but the quality of the component will also be hard to beat.
Radix’s navigation menu component.
It’s also used by many amazing companies and individuals (shadcn) in the industry, which reaffirms the quality of the component and encourages me to use it.
---
# animations.dev
- Module: Resources
- Lesson URL: /lessons/resources/animations-and-ai/
- Markdown URL: /md/lessons/resources/animations-and-ai.md
- Source mirror path: apps/anim/learn/animations-and-ai.html
All you need to know before getting started.
## Intro
All you need to know before getting started.
## What makes an animation feel right
Why some animations feel better than others.
## The Easing Blueprint
The different types of easing and when to use them.
## Spring animations
The secret ingredient.
## Timing and purpose
When and how to animate.
## Taste
Why it’s important and how to improve it.
## Animations and AI
How AI can help you create great animations.
## Practical Animation Tips
Simple ideas that can improve your animations.
## The beauty of CSS animations
What makes CSS animations so great.
## Transforms
A property that we have to understand before moving forward.
## Transitions
The basics of CSS transitions.
## Keyframe Animations
Everything you need to know about keyframe animations.
## The Magic of Clip Path
A very powerful CSS property.
## Why Framer Motion
What are the alternatives and why I use this library.
## The Basics
Basics of Framer Motion.
## How do I code animations
In-depth look at how I use Framer Motion.
## Feedback popover
A component that uses a few techniques we’ve learned.
## Multi-step component
A direction aware multi-step component.
## Trash interaction
Recreation of Family’s trash interaction on the web.
## Hooks and animations
How we can use hooks with animations
## Interactive graph
Using hooks to create an interactive graph
## Animating in public
Taking advantage of what you have learned.
## The big little details
Details that can take your animation from good to great.
## Performance
Everything you need to know about web animations performance.
## Accessibility
Great animations are accessible to everyone.
## Animations of the future
What the future holds for animations.
## The analysis
A break down of the drawer to see what makes it feel great.
## First animations
We’ll start by adding Vaul and animating the height.
## Crossfade
Crossfade effect, a crucial part of the animation.
## The finishing touch
Some final polish to make it feel even better.
## The Design
A break down to determine the requirements.
## Ring view
We’ll build the ring view with Framer Motion’s keyframes.
## Timer view
Another view, this one will reuse things we have learned.
## Morph effect
The hardest part of the Island.
## Know your tools
A lesson on the importance of knowing your tools.
## Animating the menu
Let’s build the menu.
## Closing thoughts
A smaller improvement and my thoughts on libraries.
## SVG introduction
SVG fundamentals before we dive into the animations.
## Lines and dashes
Everything you need to know about lines and dashes in SVGs.
## Rotation
The basics of rotating SVG elements.
## Click animation
Building the click animation.
## Clock animation
Building the clock animation.
## Polish
Adding subtle details to make this truly stand out.
---
# animations.dev
- Module: Resources
- Lesson URL: /lessons/resources/easing-curves/
- Markdown URL: /md/lessons/resources/easing-curves.md
- Source mirror path: apps/anim/learn/easing-curves.html
These custom curves are made by Benjamin De Cock. I use these often if a built-in easing curve is not strong enough. We also use them at Linear.
## Overview
- Intro
- What makes an animation feel right
- The Easing Blueprint
- Spring animations
- Timing and purpose
- Taste
- Animations and AIUpdated
- Practical Animation Tips
- The beauty of CSS animations
- Transforms
- Transitions
- Keyframe Animations
- The Magic of Clip Path
- Why Framer Motion
- The Basics
- How do I code animations
- Feedback popover
- Multi-step component
- Trash interaction
- Hooks and animations
- Interactive graph
- Animating in public
- The big little details
- Performance
- Accessibility
- Animations of the future
These custom curves are made by Benjamin De Cock. I use these often if a built-in easing curve is not strong enough. We also use them at Linear.
The names of these variables refer to different mathematical functions used to define them.
```css
:root {
--ease-in-quad: cubic-bezier(.55, .085, .68, .53);
--ease-in-cubic: cubic-bezier(.550, .055, .675, .19);
--ease-in-quart: cubic-bezier(.895, .03, .685, .22);
--ease-in-quint: cubic-bezier(.755, .05, .855, .06);
--ease-in-expo: cubic-bezier(.95, .05, .795, .035);
--ease-in-circ: cubic-bezier(.6, .04, .98, .335);
--ease-out-quad: cubic-bezier(.25, .46, .45, .94);
--ease-out-cubic: cubic-bezier(.215, .61, .355, 1);
--ease-out-quart: cubic-bezier(.165, .84, .44, 1);
--ease-out-quint: cubic-bezier(.23, 1, .32, 1);
--ease-out-expo: cubic-bezier(.19, 1, .22, 1);
--ease-out-circ: cubic-bezier(.075, .82, .165, 1);
--ease-in-out-quad: cubic-bezier(.455, .03, .515, .955);
--ease-in-out-cubic: cubic-bezier(.645, .045, .355, 1);
--ease-in-out-quart: cubic-bezier(.77, 0, .175, 1);
--ease-in-out-quint: cubic-bezier(.86, 0, .07, 1);
--ease-in-out-expo: cubic-bezier(1, 0, 0, 1);
--ease-in-out-circ: cubic-bezier(.785, .135, .15, .86);
}
```
```css
:root {
--ease-in-quad: cubic-bezier(.55, .085, .68, .53);
--ease-in-cubic: cubic-bezier(.550, .055, .675, .19);
--ease-in-quart: cubic-bezier(.895, .03, .685, .22);
--ease-in-quint: cubic-bezier(.755, .05, .855, .06);
--ease-in-expo: cubic-bezier(.95, .05, .795, .035);
--ease-in-circ: cubic-bezier(.6, .04, .98, .335);
--ease-out-quad: cubic-bezier(.25, .46, .45, .94);
--ease-out-cubic: cubic-bezier(.215, .61, .355, 1);
--ease-out-quart: cubic-bezier(.165, .84, .44, 1);
--ease-out-quint: cubic-bezier(.23, 1, .32, 1);
--ease-out-expo: cubic-bezier(.19, 1, .22, 1);
--ease-out-circ: cubic-bezier(.075, .82, .165, 1);
--ease-in-out-quad: cubic-bezier(.455, .03, .515, .955);
--ease-in-out-cubic: cubic-bezier(.645, .045, .355, 1);
--ease-in-out-quart: cubic-bezier(.77, 0, .175, 1);
--ease-in-out-quint: cubic-bezier(.86, 0, .07, 1);
--ease-in-out-expo: cubic-bezier(1, 0, 0, 1);
--ease-in-out-circ: cubic-bezier(.785, .135, .15, .86);
}
```
---
# animations.dev
- Module: Resources
- Lesson URL: /lessons/resources/emil-skill/
- Markdown URL: /md/lessons/resources/emil-skill.md
- Source mirror path: apps/anim/learn/emil-skill.html
A comprehensive skill for agents based on my years of design engineering experience. Meant to instantly improve the quality of your output, whether it’s design or code.
## Overview
A comprehensive skill for agents based on my years of design engineering experience. Meant to instantly improve the quality of your output, whether it’s design or code.
## Note
This is what I wish I had when I was starting out as a design engineer.
It contains everything I’ve learned from working at companies like Vercel and Linear, and building open source libraries like Sonner and Vaul.
For animations.dev students only.
“Just got my hands on Emil’s skill file, it is gold. His course was already good, but this contains so much more information and things you normally need to think of.”
## What’s inside
In addition to the animation skill that you get completely for free in Animations and AI lesson, this skill also contains separate files on:
```text
┌───────────────────────┬─────────────────────────────────────────────────────────┐
│ Category │ Covers │
├───────────────────────┼─────────────────────────────────────────────────────────┤
│ UI Polish │ Typography, shadows, gradients, layout, dark mode │
├───────────────────────┼─────────────────────────────────────────────────────────┤
│ Forms & Controls │ Inputs, buttons, form patterns, validation │
├───────────────────────┼─────────────────────────────────────────────────────────┤
│ Touch & Accessibility │ Touch devices, keyboard nav, a11y, tap targets │
├───────────────────────┼─────────────────────────────────────────────────────────┤
│ Component Design │ Compound components, composition, customizability │
├───────────────────────┼─────────────────────────────────────────────────────────┤
│ Marketing │ Landing pages, blogs, docs, changelogs │
├───────────────────────┼─────────────────────────────────────────────────────────┤
│ Performance │ Virtualization, preloading, optimization │
└───────────────────────┴─────────────────────────────────────────────────────────┘
```
More categories coming soon.
Some of the things mentioned in these files won’t be new to you, but there are also bits that I’ve never shared before and are exclusive for this skill. Here are a few of them:
## Eased Gradients
Use eased gradients over linear gradients when using solid colors. Linear gradients have visible banding; eased gradients are smoother.
```css
.gradient {
background: linear-gradient(to bottom, hsl(330, 100%, 45.1%) 0%, hsl(331, 89.25%, 47.36%) 8.1%, hsl(330.53, 79.69%, 48.96%) 15.5%, hsl(328.56, 70.89%, 49.96%) 22.5%, hsl(324.94, 63.52%, 50.4%) 29%, hsl(319.21, 54.99%, 50.3%) 35.3%, hsl(310.39, 46.14%, 49.68%) 41.2%, hsl(296.53, 39.12%, 49.7%) 47.1%, hsl(280.63, 42.91%, 53.43%) 52.9%, hsl(265.14, 47.59%, 56.84%) 58.8%, hsl(250.13, 52.52%, 59.88%) 64.7%, hsl(235.88, 59.2%, 60.91%) 71%, hsl(225.81, 68.23%, 57.85%) 77.5%, hsl(218.93, 74.97%, 54.21%) 84.5%, hsl(213.89, 79.63%, 49.97%) 91.9%, hsl(210, 100%, 45.1%) 100%);
}
```
```css
.gradient {
background: linear-gradient(to bottom, hsl(330, 100%, 45.1%) 0%, hsl(331, 89.25%, 47.36%) 8.1%, hsl(330.53, 79.69%, 48.96%) 15.5%, hsl(328.56, 70.89%, 49.96%) 22.5%, hsl(324.94, 63.52%, 50.4%) 29%, hsl(319.21, 54.99%, 50.3%) 35.3%, hsl(310.39, 46.14%, 49.68%) 41.2%, hsl(296.53, 39.12%, 49.7%) 47.1%, hsl(280.63, 42.91%, 53.43%) 52.9%, hsl(265.14, 47.59%, 56.84%) 58.8%, hsl(250.13, 52.52%, 59.88%) 64.7%, hsl(235.88, 59.2%, 60.91%) 71%, hsl(225.81, 68.23%, 57.85%) 77.5%, hsl(218.93, 74.97%, 54.21%) 84.5%, hsl(213.89, 79.63%, 49.97%) 91.9%, hsl(210, 100%, 45.1%) 100%);
}
```
This skill is not finished. Just like I kept my word and the course has been receiving consistent updates since the initial release, this skill will also be updated based on new learnings, feedback, past experiences, and new developments in the AI world.
“The Emil Design Engineering skill is really impressive. I just used it to help me build an infinite scroll behavior on a table and it gave me some really nice recommendations . I even used it outside of its main goal to make my Obsidian custom theme better. It's really one of my favorite skills out there favorite skills out there”
You pay once, and get lifetime access to all updates. It’s an early access price for those that want to try it out and help me make it better. The price will only go up in the future.
---
# Interviews
- Module: Resources
- Lesson URL: /lessons/resources/interviews/
- Markdown URL: /md/lessons/resources/interviews.md
- Source mirror path: apps/anim/learn/interviews.html
Talking about animations, design, taste, career, and more.
## Overview
Talking about animations, design, taste, career, and more.
## Henry Heffernan
Design engineer at Vercel working on v0.
## Mariana Castilho
Product designer at poolside, previously Vercel.
## Lochie Axon
Design engineer at Family.
## Dennis Brotzky
Co-founder of Fey, works mainly as an engineer.
---