# 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 (
); } ``` ```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

A series of diagonal black and white stripes with a smooth gradient effect. The alternating light and dark bands create a sense of depth and movement, resembling light rays or shadows cast across a surface. The overall aesthetic is abstract and high-contrast, with a sleek, modern feel. ); } ``` 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 (

Project name

Project description

); } ``` ## 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 (
{Array.from({ length: toasts }).map((_, i) => ( ))}
); } function Toast() { return (
Event Created Monday, January 3rd at 6:00pm
); } ``` ## 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 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.

) : (
{ e.preventDefault(); if(!feedback) return; submit(); }} className="feedback-form" >