# lessons

id: animation-theory/animations-and-ai
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: animations-and-ai
title: Animations and AI
url_path: /lessons/animation-theory/animations-and-ai/
markdown_path: /md/lessons/animation-theory/animations-and-ai.md
source_path: apps/anim/learn/animation-theory/animations-and-ai.html
excerpt:
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.


id: animation-theory/practical-animation-tips
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: practical-animation-tips
title: Practical Animation Tips
url_path: /lessons/animation-theory/practical-animation-tips/
markdown_path: /md/lessons/animation-theory/practical-animation-tips.md
source_path: apps/anim/learn/animation-theory/practical-animation-tips.html
excerpt:
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.


id: animation-theory/spring-animations
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: spring-animations
title: Spring animations
url_path: /lessons/animation-theory/spring-animations/
markdown_path: /md/lessons/animation-theory/spring-animations.md
source_path: apps/anim/learn/animation-theory/spring-animations.html
excerpt:
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.


id: animation-theory/taste
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: taste
title: Taste
url_path: /lessons/animation-theory/taste/
markdown_path: /md/lessons/animation-theory/taste.md
source_path: apps/anim/learn/animation-theory/taste.html
excerpt:
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.


id: animation-theory/the-easing-blueprint
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: the-easing-blueprint
title: The Easing Blueprint
url_path: /lessons/animation-theory/the-easing-blueprint/
markdown_path: /md/lessons/animation-theory/the-easing-blueprint.md
source_path: apps/anim/learn/animation-theory/the-easing-blueprint.html
excerpt:
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.


id: animation-theory/timing-and-purpose
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: timing-and-purpose
title: Timing and purpose
url_path: /lessons/animation-theory/timing-and-purpose/
markdown_path: /md/lessons/animation-theory/timing-and-purpose.md
source_path: apps/anim/learn/animation-theory/timing-and-purpose.html
excerpt:
When done right, animations make an interface feel predictable, faster, and more enjoyable to use. They help you and your product stand out.


id: animation-theory/what-makes-an-animation-feel-right
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: what-makes-an-animation-feel-right
title: What makes an animation feel right?
url_path: /lessons/animation-theory/what-makes-an-animation-feel-right/
markdown_path: /md/lessons/animation-theory/what-makes-an-animation-feel-right.md
source_path: apps/anim/learn/animation-theory/what-makes-an-animation-feel-right.html
excerpt:
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.


id: css-animations/keyframe-animations
module_slug: css-animations
module_title: CSS Animations
lesson_slug: keyframe-animations
title: Keyframe Animations
url_path: /lessons/css-animations/keyframe-animations/
markdown_path: /md/lessons/css-animations/keyframe-animations.md
source_path: apps/anim/learn/css-animations/keyframe-animations.html
excerpt:
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.


id: css-animations/the-beauty-of-css-animations
module_slug: css-animations
module_title: CSS Animations
lesson_slug: the-beauty-of-css-animations
title: The beauty of CSS animations
url_path: /lessons/css-animations/the-beauty-of-css-animations/
markdown_path: /md/lessons/css-animations/the-beauty-of-css-animations.md
source_path: apps/anim/learn/css-animations/the-beauty-of-css-animations.html
excerpt:
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.


id: css-animations/the-magic-of-clip-path
module_slug: css-animations
module_title: CSS Animations
lesson_slug: the-magic-of-clip-path
title: The Magic of Clip Path
url_path: /lessons/css-animations/the-magic-of-clip-path/
markdown_path: /md/lessons/css-animations/the-magic-of-clip-path.md
source_path: apps/anim/learn/css-animations/the-magic-of-clip-path.html
excerpt:
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?


id: css-animations/transforms
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transforms
title: Transforms
url_path: /lessons/css-animations/transforms/
markdown_path: /md/lessons/css-animations/transforms.md
source_path: apps/anim/learn/css-animations/transforms.html
excerpt:
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.


id: css-animations/transitions
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transitions
title: Transitions
url_path: /lessons/css-animations/transitions/
markdown_path: /md/lessons/css-animations/transitions.md
source_path: apps/anim/learn/css-animations/transitions.html
excerpt:
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.


id: dynamic-island/morph-effect
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: morph-effect
title: Morph effect
url_path: /lessons/dynamic-island/morph-effect/
markdown_path: /md/lessons/dynamic-island/morph-effect.md
source_path: apps/anim/learn/dynamic-island/morph-effect.html
excerpt:
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.


id: dynamic-island/ring-view
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: ring-view
title: Ring view
url_path: /lessons/dynamic-island/ring-view/
markdown_path: /md/lessons/dynamic-island/ring-view.md
source_path: apps/anim/learn/dynamic-island/ring-view.html
excerpt:
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.


id: dynamic-island/the-design
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: the-design
title: The Design
url_path: /lessons/dynamic-island/the-design/
markdown_path: /md/lessons/dynamic-island/the-design.md
source_path: apps/anim/learn/dynamic-island/the-design.html
excerpt:
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.


id: dynamic-island/timer-view
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: timer-view
title: Timer view
url_path: /lessons/dynamic-island/timer-view/
markdown_path: /md/lessons/dynamic-island/timer-view.md
source_path: apps/anim/learn/dynamic-island/timer-view.html
excerpt:
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.


id: family-drawer/crossfade
module_slug: family-drawer
module_title: Family Drawer
lesson_slug: crossfade
title: Crossfade
url_path: /lessons/family-drawer/crossfade/
markdown_path: /md/lessons/family-drawer/crossfade.md
source_path: apps/anim/learn/family-drawer/crossfade.html
excerpt:
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.


id: family-drawer/first-animations
module_slug: family-drawer
module_title: Family Drawer
lesson_slug: first-animations
title: First animations
url_path: /lessons/family-drawer/first-animations/
markdown_path: /md/lessons/family-drawer/first-animations.md
source_path: apps/anim/learn/family-drawer/first-animations.html
excerpt:
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.


id: family-drawer/the-analysis
module_slug: family-drawer
module_title: Family Drawer
lesson_slug: the-analysis
title: The analysis
url_path: /lessons/family-drawer/the-analysis/
markdown_path: /md/lessons/family-drawer/the-analysis.md
source_path: apps/anim/learn/family-drawer/the-analysis.html
excerpt:
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.


id: family-drawer/the-finishing-touch
module_slug: family-drawer
module_title: Family Drawer
lesson_slug: the-finishing-touch
title: The finishing touch
url_path: /lessons/family-drawer/the-finishing-touch/
markdown_path: /md/lessons/family-drawer/the-finishing-touch.md
source_path: apps/anim/learn/family-drawer/the-finishing-touch.html
excerpt:
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.


id: framer-motion/hooks-and-animations
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: hooks-and-animations
title: Hooks and animations
url_path: /lessons/framer-motion/hooks-and-animations/
markdown_path: /md/lessons/framer-motion/hooks-and-animations.md
source_path: apps/anim/learn/framer-motion/hooks-and-animations.html
excerpt:
We’ve now animated things mainly using initial, animate and exit props. While these props are very powerful, and I personally use Framer Motion this way ~90% of the time, this library has more to offer.


id: framer-motion/interactive-graph
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: interactive-graph
title: Interactive graph
url_path: /lessons/framer-motion/interactive-graph/
markdown_path: /md/lessons/framer-motion/interactive-graph.md
source_path: apps/anim/learn/framer-motion/interactive-graph.html
excerpt:
In this lesson we’ll create an interactive graph to solidify our understanding of the hooks we just learned, but also to learn an additional hook. As always, it’s a real-world example, you can see a very similar component on Linear’s features page. This is how the end result will look like:


id: framer-motion/multi-step-component
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: multi-step-component
title: Multi-step component
url_path: /lessons/framer-motion/multi-step-component/
markdown_path: /md/lessons/framer-motion/multi-step-component.md
source_path: apps/anim/learn/framer-motion/multi-step-component.html
excerpt:
Usually in this step we would explain why this thing exists and what it does. Also, we would show a button to go to the next step.


id: framer-motion/the-basics
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: the-basics
title: The Basics
url_path: /lessons/framer-motion/the-basics/
markdown_path: /md/lessons/framer-motion/the-basics.md
source_path: apps/anim/learn/framer-motion/the-basics.html
excerpt:
While Framer Motion is a very powerful animation library and is relatively easy to start with, it still differs from CSS animations. We’ll go over the basics first and get comfortable with the library.


id: framer-motion/trash-interaction
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: trash-interaction
title: Trash interaction
url_path: /lessons/framer-motion/trash-interaction/
markdown_path: /md/lessons/framer-motion/trash-interaction.md
source_path: apps/anim/learn/framer-motion/trash-interaction.html
excerpt:
To get the codebase running locally, you first need to download it here. We switched from a GitHub repo, because it would be quite problematic to keep it private.


id: framer-motion/why-framer-motion
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: why-framer-motion
title: Why Framer Motion
url_path: /lessons/framer-motion/why-framer-motion/
markdown_path: /md/lessons/framer-motion/why-framer-motion.md
source_path: apps/anim/learn/framer-motion/why-framer-motion.html
excerpt:
Framer Motion allows us to create very impressive, native-like animations with not a lot of code. Because of that, many things happen magically, and once you run into an issue it can be hard to debug.


id: good-vs-great-animations/accessibility
module_slug: good-vs-great-animations
module_title: Good vs Great Animations
lesson_slug: accessibility
title: Accessibility
url_path: /lessons/good-vs-great-animations/accessibility/
markdown_path: /md/lessons/good-vs-great-animations/accessibility.md
source_path: apps/anim/learn/good-vs-great-animations/accessibility.html
excerpt:
The interactive examples and code snippets in this article are crucial to understanding the concepts like in the last lesson, so this one is text-based as well, but there is an exercise with a video for the solution.


id: good-vs-great-animations/animations-of-the-future
module_slug: good-vs-great-animations
module_title: Good vs Great Animations
lesson_slug: animations-of-the-future
title: Animations of the future
url_path: /lessons/good-vs-great-animations/animations-of-the-future/
markdown_path: /md/lessons/good-vs-great-animations/animations-of-the-future.md
source_path: apps/anim/learn/good-vs-great-animations/animations-of-the-future.html
excerpt:
Static transitions can disrupt the user’s sense of flow and orientation. You should envision your interface as a constantly evolving space, where any element can transform into another.


id: good-vs-great-animations/performance
module_slug: good-vs-great-animations
module_title: Good vs Great Animations
lesson_slug: performance
title: Performance
url_path: /lessons/good-vs-great-animations/performance/
markdown_path: /md/lessons/good-vs-great-animations/performance.md
source_path: apps/anim/learn/good-vs-great-animations/performance.html
excerpt:
The interactive examples in this article are crucial to understanding the concepts, which is why I decided to make this lesson text-only.


id: good-vs-great-animations/the-big-little-details
module_slug: good-vs-great-animations
module_title: Good vs Great Animations
lesson_slug: the-big-little-details
title: The big little details
url_path: /lessons/good-vs-great-animations/the-big-little-details/
markdown_path: /md/lessons/good-vs-great-animations/the-big-little-details.md
source_path: apps/anim/learn/good-vs-great-animations/the-big-little-details.html
excerpt:
We’ve talked about the theory behind good animations and how to actually code them. Now it’s time to talk about the details that separate good animations from great animations. And we’ll start with feeling.


id: hero-illustration/click-animation
module_slug: hero-illustration
module_title: Hero Illustration
lesson_slug: click-animation
title: Click animation
url_path: /lessons/hero-illustration/click-animation/
markdown_path: /md/lessons/hero-illustration/click-animation.md
source_path: apps/anim/learn/hero-illustration/click-animation.html
excerpt:
In this lesson we’ll build the click animation. We’ll layer transform-based animations, path morphing, and stroke dash effects with careful timing to make everything feel unified.


id: hero-illustration/clock-animation
module_slug: hero-illustration
module_title: Hero Illustration
lesson_slug: clock-animation
title: Clock animation
url_path: /lessons/hero-illustration/clock-animation/
markdown_path: /md/lessons/hero-illustration/clock-animation.md
source_path: apps/anim/learn/hero-illustration/clock-animation.html
excerpt:
This clock has a fun easter egg - click it and the hands will spin to show you the actual time. We’ll build this animation step by step: bells swinging sideways in idle state, alarm going off on hover, current time animation on click and feedback on subsequent clicks.


id: hero-illustration/lines-and-dashes
module_slug: hero-illustration
module_title: Hero Illustration
lesson_slug: lines-and-dashes
title: Lines and dashes
url_path: /lessons/hero-illustration/lines-and-dashes/
markdown_path: /md/lessons/hero-illustration/lines-and-dashes.md
source_path: apps/anim/learn/hero-illustration/lines-and-dashes.html
excerpt:
You’ve probably seen this effect before, a path that looks like it’s drawing itself. Loading spinners, signature animations, icon reveals. It’s everywhere. In this case, it’s the hand’s rays.


id: hero-illustration/polish
module_slug: hero-illustration
module_title: Hero Illustration
lesson_slug: polish
title: Polish
url_path: /lessons/hero-illustration/polish/
markdown_path: /md/lessons/hero-illustration/polish.md
source_path: apps/anim/learn/hero-illustration/polish.html
excerpt:
The clock works. It responds to hover, animates the hands to the current time, handles all the interactions. But something’s missing. That subtle aliveness you see in UIs that truly stand out. Let’s improve it!


id: hero-illustration/rotation
module_slug: hero-illustration
module_title: Hero Illustration
lesson_slug: rotation
title: Rotation
url_path: /lessons/hero-illustration/rotation/
markdown_path: /md/lessons/hero-illustration/rotation.md
source_path: apps/anim/learn/hero-illustration/rotation.html
excerpt:
So, we know how to position shapes and animate lines. Now let’s rotate them. Click the clock below to see the hands rotate around the clock center.


id: hero-illustration/svg-introduction
module_slug: hero-illustration
module_title: Hero Illustration
lesson_slug: svg-introduction
title: SVG Introduction
url_path: /lessons/hero-illustration/svg-introduction/
markdown_path: /md/lessons/hero-illustration/svg-introduction.md
source_path: apps/anim/learn/hero-illustration/svg-introduction.html
excerpt:
A while back me and dimi worked together on some animations of the illustrations on animations.dev. Hover on each of the boxes above to see the animations in action. This walkthrough will cover how we animated some of them.


id: interviews/dennis-brotzky
module_slug: interviews
module_title: Interviews
lesson_slug: dennis-brotzky
title: Interviews/Dennis Brotzky
url_path: /lessons/interviews/dennis-brotzky/
markdown_path: /md/lessons/interviews/dennis-brotzky.md
source_path: apps/anim/learn/interviews/dennis-brotzky.html
excerpt:
Brotzky is an engineer who loves design. He is also the co-founder of Fey, an app that helps you make better investments.


id: interviews/henry-heffernan
module_slug: interviews
module_title: Interviews
lesson_slug: henry-heffernan
title: Interviews/Henry Heffernan
url_path: /lessons/interviews/henry-heffernan/
markdown_path: /md/lessons/interviews/henry-heffernan.md
source_path: apps/anim/learn/interviews/henry-heffernan.html
excerpt:
Henry is a design engineer working at Vercel. He contributed to a lot of Vercel’s new marketing pages, Next.js Conf, etc. He also has a very impressive personal site. In this interview, we’ll talk about animations, easings, design engineer in general and how he got his job at Vercel.


id: interviews/lochie-axon
module_slug: interviews
module_title: Interviews
lesson_slug: lochie-axon
title: Interviews/Lochie Axon
url_path: /lessons/interviews/lochie-axon/
markdown_path: /md/lessons/interviews/lochie-axon.md
source_path: apps/anim/learn/interviews/lochie-axon.html
excerpt:
Lochie is a design engineer working at Avara. He built the Family site, and docs, ConnectKit, Avara’s site, and many other great things. In this conversation we’ll how Lochie got into design engineering, how he thinks about animations, and how he builds them.


id: interviews/mariana-castilho
module_slug: interviews
module_title: Interviews
lesson_slug: mariana-castilho
title: Interviews/Mariana Castilho
url_path: /lessons/interviews/mariana-castilho/
markdown_path: /md/lessons/interviews/mariana-castilho.md
source_path: apps/anim/learn/interviews/mariana-castilho.html
excerpt:
Mariana is a Brazilian product designer currently working at pooolside. She created a project called uilabs.dev where she shares her UI explorations. In this interview we are going to talk about product design, how she learned to code as a designer, and how that influenced her design proces


id: navigation-menu/animating-the-menu
module_slug: navigation-menu
module_title: Navigation Menu
lesson_slug: animating-the-menu
title: Animating the menu
url_path: /lessons/navigation-menu/animating-the-menu/
markdown_path: /md/lessons/navigation-menu/animating-the-menu.md
source_path: apps/anim/learn/navigation-menu/animating-the-menu.html
excerpt:
Let’s try and build our menu. This is how our menu will look like after we’re done with the exercises:


id: navigation-menu/closing-thoughts
module_slug: navigation-menu
module_title: Navigation Menu
lesson_slug: closing-thoughts
title: Closing thoughts
url_path: /lessons/navigation-menu/closing-thoughts/
markdown_path: /md/lessons/navigation-menu/closing-thoughts.md
source_path: apps/anim/learn/navigation-menu/closing-thoughts.html
excerpt:
Our menu is basically finished at this point. In this lesson I’ll discuss a small improvement we can make and we’ll talk about choosing libraries.


id: navigation-menu/know-your-tools
module_slug: navigation-menu
module_title: Navigation Menu
lesson_slug: know-your-tools
title: Know your tools
url_path: /lessons/navigation-menu/know-your-tools/
markdown_path: /md/lessons/navigation-menu/know-your-tools.md
source_path: apps/anim/learn/navigation-menu/know-your-tools.html
excerpt:
Let’s assume we work on the marketing team at a company and we are tasked with creating a navigation menu with multiple dropdowns. How would should we go about this?


id: resources/easing-curves
module_slug: resources
module_title: Resources
lesson_slug: easing-curves
title: animations.dev
url_path: /lessons/resources/easing-curves/
markdown_path: /md/lessons/resources/easing-curves.md
source_path: apps/anim/learn/easing-curves.html
excerpt:
These custom curves are made by Benjamin De Cock. I use these often if a built-in easing curve is not strong enough. We also use them at Linear.


id: resources/emil-skill
module_slug: resources
module_title: Resources
lesson_slug: emil-skill
title: animations.dev
url_path: /lessons/resources/emil-skill/
markdown_path: /md/lessons/resources/emil-skill.md
source_path: apps/anim/learn/emil-skill.html
excerpt:
A comprehensive skill for agents based on my years of design engineering experience. Meant to instantly improve the quality of your output, whether it’s design or code.


# sections

id: animation-theory/animations-and-ai/overview
lesson_id: animation-theory/animations-and-ai
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: animations-and-ai
lesson_title: Animations and AI
heading: Overview
url_path: /lessons/animation-theory/animations-and-ai/#overview
markdown_path: /md/sections/animation-theory/animations-and-ai/overview.md
excerpt:
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.

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


id: animation-theory/animations-and-ai/how-ai-can-help-you
lesson_id: animation-theory/animations-and-ai
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: animations-and-ai
lesson_title: Animations and AI
heading: How AI can help you
url_path: /lessons/animation-theory/animations-and-ai/#how-ai-can-help-you
markdown_path: /md/sections/animation-theory/animations-and-ai/how-ai-can-help-you.md
excerpt:
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.

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


id: animation-theory/animations-and-ai/installation
lesson_id: animation-theory/animations-and-ai
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: animations-and-ai
lesson_title: Animations and AI
heading: Installation
excerpt: Run this command in your terminal:
url_path: /lessons/animation-theory/animations-and-ai/#installation
markdown_path: /md/sections/animation-theory/animations-and-ai/installation.md
body:
Run this command in your terminal:

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.


id: animation-theory/animations-and-ai/how-do-i-use-ai
lesson_id: animation-theory/animations-and-ai
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: animations-and-ai
lesson_title: Animations and AI
heading: How do I use AI?
url_path: /lessons/animation-theory/animations-and-ai/#how-do-i-use-ai
markdown_path: /md/sections/animation-theory/animations-and-ai/how-do-i-use-ai.md
excerpt:
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.

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


id: animation-theory/animations-and-ai/game-of-the-day
lesson_id: animation-theory/animations-and-ai
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: animations-and-ai
lesson_title: Animations and AI
heading: Game of the day
excerpt: A game about vikings
url_path: /lessons/animation-theory/animations-and-ai/#game-of-the-day
markdown_path: /md/sections/animation-theory/animations-and-ai/game-of-the-day.md
body:
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.


id: animation-theory/intro/overview
lesson_id: animation-theory/intro
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: intro
lesson_title: Intro
heading: Overview
excerpt: Welcome to animations.dev!
url_path: /lessons/animation-theory/intro/#overview
markdown_path: /md/sections/animation-theory/intro/overview.md
body:
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.


id: animation-theory/intro/the-structure
lesson_id: animation-theory/intro
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: intro
lesson_title: Intro
heading: The structure
excerpt: This course is split into four modules and a series of walkthroughs.
url_path: /lessons/animation-theory/intro/#the-structure
markdown_path: /md/sections/animation-theory/intro/the-structure.md
body:
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.


id: animation-theory/intro/lesson-structure
lesson_id: animation-theory/intro
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: intro
lesson_title: Intro
heading: Lesson structure
url_path: /lessons/animation-theory/intro/#lesson-structure
markdown_path: /md/sections/animation-theory/intro/lesson-structure.md
excerpt:
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.

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


id: animation-theory/intro/code-playgrounds
lesson_id: animation-theory/intro
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: intro
lesson_title: Intro
heading: Code Playgrounds
url_path: /lessons/animation-theory/intro/#code-playgrounds
markdown_path: /md/sections/animation-theory/intro/code-playgrounds.md
excerpt:
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:

body:
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:

import { motion } from "motion/react";
import { useState } from "react";

export default function Example() {
return (
<div className="wrapper">
    <motion.div
      initial={{ opacity: 0, scale: 0 }}
      animate={{ opacity: 1, scale: 1 }}
      className="element"
    >
    <h1>Hello</h1>
    </motion.div>
</div>
)
}

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.


id: animation-theory/intro/progress-tracking
lesson_id: animation-theory/intro
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: intro
lesson_title: Intro
heading: Progress tracking
url_path: /lessons/animation-theory/intro/#progress-tracking
markdown_path: /md/sections/animation-theory/intro/progress-tracking.md
excerpt:
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.

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


id: animation-theory/intro/how-to-get-the-most-out-of-this-course
lesson_id: animation-theory/intro
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: intro
lesson_title: Intro
heading: How to get the most out of this course?
url_path: /lessons/animation-theory/intro/#how-to-get-the-most-out-of-this-course
markdown_path: /md/sections/animation-theory/intro/how-to-get-the-most-out-of-this-course.md
excerpt:
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.

body:
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!


id: animation-theory/intro/bonus-features
lesson_id: animation-theory/intro
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: intro
lesson_title: Intro
heading: Bonus features
url_path: /lessons/animation-theory/intro/#bonus-features
markdown_path: /md/sections/animation-theory/intro/bonus-features.md
excerpt:
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.

body:
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


id: animation-theory/practical-animation-tips/repetition
lesson_id: animation-theory/practical-animation-tips
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: practical-animation-tips
lesson_title: Practical Animation Tips
heading: Repetition
url_path: /lessons/animation-theory/practical-animation-tips/#repetition
markdown_path: /md/sections/animation-theory/practical-animation-tips/repetition.md
excerpt:
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.

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


id: animation-theory/practical-animation-tips/record-your-animations
lesson_id: animation-theory/practical-animation-tips
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: practical-animation-tips
lesson_title: Practical Animation Tips
heading: Record your animations
url_path: /lessons/animation-theory/practical-animation-tips/#record-your-animations
markdown_path: /md/sections/animation-theory/practical-animation-tips/record-your-animations.md
excerpt:
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.

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


id: animation-theory/practical-animation-tips/fix-shaky-animations
lesson_id: animation-theory/practical-animation-tips
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: practical-animation-tips
lesson_title: Practical Animation Tips
heading: Fix shaky animations
url_path: /lessons/animation-theory/practical-animation-tips/#fix-shaky-animations
markdown_path: /md/sections/animation-theory/practical-animation-tips/fix-shaky-animations.md
excerpt:
When you animate using CSS’s transform, the element might shift by 1px at the start and end of the transition in some browsers.

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

.element {
  will-change: transform;
}

.element {
  will-change: transform;
}


id: animation-theory/practical-animation-tips/give-yourself-a-break
lesson_id: animation-theory/practical-animation-tips
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: practical-animation-tips
lesson_title: Practical Animation Tips
heading: Give yourself a break
url_path: /lessons/animation-theory/practical-animation-tips/#give-yourself-a-break
markdown_path: /md/sections/animation-theory/practical-animation-tips/give-yourself-a-break.md
excerpt:
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.

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


id: animation-theory/practical-animation-tips/scale-your-buttons
lesson_id: animation-theory/practical-animation-tips
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: practical-animation-tips
lesson_title: Practical Animation Tips
heading: Scale your buttons
url_path: /lessons/animation-theory/practical-animation-tips/#scale-your-buttons
markdown_path: /md/sections/animation-theory/practical-animation-tips/scale-your-buttons.md
excerpt:
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.

body:
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:


id: animation-theory/practical-animation-tips/don-t-animate-from-scale-0
lesson_id: animation-theory/practical-animation-tips
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: practical-animation-tips
lesson_title: Practical Animation Tips
heading: Don’t animate from scale(0)
url_path: /lessons/animation-theory/practical-animation-tips/#don-t-animate-from-scale-0
markdown_path: /md/sections/animation-theory/practical-animation-tips/don-t-animate-from-scale-0.md
excerpt:
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.

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


id: animation-theory/practical-animation-tips/don-t-animate-subsequent-tooltips
lesson_id: animation-theory/practical-animation-tips
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: practical-animation-tips
lesson_title: Practical Animation Tips
heading: Don’t animate subsequent tooltips
url_path: /lessons/animation-theory/practical-animation-tips/#don-t-animate-subsequent-tooltips
markdown_path: /md/sections/animation-theory/practical-animation-tips/don-t-animate-subsequent-tooltips.md
excerpt:
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.

body:
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:

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

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


id: animation-theory/practical-animation-tips/make-your-animations-origin-aware
lesson_id: animation-theory/practical-animation-tips
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: practical-animation-tips
lesson_title: Practical Animation Tips
heading: Make your animations origin aware
url_path: /lessons/animation-theory/practical-animation-tips/#make-your-animations-origin-aware
markdown_path: /md/sections/animation-theory/practical-animation-tips/make-your-animations-origin-aware.md
excerpt:
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.

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

.radix {
  transform-origin: var(--radix-dropdown-menu-content-transform-origin);
}

.baseui {
  transform-origin: var(--transform-origin);
}

.radix {
  transform-origin: var(--radix-dropdown-menu-content-transform-origin);
}

.baseui {
  transform-origin: var(--transform-origin);
}


id: animation-theory/practical-animation-tips/keep-your-animations-fast
lesson_id: animation-theory/practical-animation-tips
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: practical-animation-tips
lesson_title: Practical Animation Tips
heading: Keep your animations fast
url_path: /lessons/animation-theory/practical-animation-tips/#keep-your-animations-fast
markdown_path: /md/sections/animation-theory/practical-animation-tips/keep-your-animations-fast.md
excerpt:
A faster-spinning spinner makes the app seem to load faster, even though the load time is the same. This improves the perceived performance.

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


id: animation-theory/practical-animation-tips/don-t-animate-keyboard-interactions
lesson_id: animation-theory/practical-animation-tips
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: practical-animation-tips
lesson_title: Practical Animation Tips
heading: Don’t animate keyboard interactions
url_path: /lessons/animation-theory/practical-animation-tips/#don-t-animate-keyboard-interactions
markdown_path: /md/sections/animation-theory/practical-animation-tips/don-t-animate-keyboard-interactions.md
excerpt:
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.

body:
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


lesson_id: animation-theory/practical-animation-tips
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: practical-animation-tips
lesson_title: Practical Animation Tips
heading: Be careful with adding animations to frequently used elements
id:
animation-theory/practical-animation-tips/be-careful-with-adding-animations-to-frequently-used-elements

excerpt:
A hover effect is nice, but if used multiple times a day, it would likely benefit the most from having no animation at all.

url_path:
/lessons/animation-theory/practical-animation-tips/#be-careful-with-adding-animations-to-frequently-used-elements

markdown_path:
/md/sections/animation-theory/practical-animation-tips/be-careful-with-adding-animations-to-frequently-used-elements.md

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


id: animation-theory/practical-animation-tips/hover-flicker
lesson_id: animation-theory/practical-animation-tips
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: practical-animation-tips
lesson_title: Practical Animation Tips
heading: Hover flicker
url_path: /lessons/animation-theory/practical-animation-tips/#hover-flicker
markdown_path: /md/sections/animation-theory/practical-animation-tips/hover-flicker.md
excerpt:
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.

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

import "./styles.css";

export default function SimpleTransformTransition() {
  return (
     <div className="box">
        <div className="box-inner" />
     </div>
  );
}

import "./styles.css";

export default function SimpleTransformTransition() {
  return (
     <div className="box">
        <div className="box-inner" />
     </div>
  );
}

.box:hover .box-inner {
  transform: translateY(-20%);
}

.box-inner {
  height: 56px;
  width: 56px;
  background: #fad655;
  border-radius: 50%;
  transition: transform 200ms ease;
}

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


id: animation-theory/practical-animation-tips/appropriate-target-area
lesson_id: animation-theory/practical-animation-tips
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: practical-animation-tips
lesson_title: Practical Animation Tips
heading: Appropriate target area
url_path: /lessons/animation-theory/practical-animation-tips/#appropriate-target-area
markdown_path: /md/sections/animation-theory/practical-animation-tips/appropriate-target-area.md
excerpt:
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.

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

@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;
  }
}

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

<button className="touch-hitbox">
  <BellIcon />
</button>

<button className="touch-hitbox">
  <BellIcon />
</button>

Thanks to Freddie for sharing this on X.


id: animation-theory/practical-animation-tips/use-ease-out-for-enter-and-exit-animations
lesson_id: animation-theory/practical-animation-tips
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: practical-animation-tips
lesson_title: Practical Animation Tips
heading: Use ease-out for enter and exit animations
url_path: /lessons/animation-theory/practical-animation-tips/#use-ease-out-for-enter-and-exit-animations
excerpt:
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.

markdown_path:
/md/sections/animation-theory/practical-animation-tips/use-ease-out-for-enter-and-exit-animations.md

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


lesson_id: animation-theory/practical-animation-tips
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: practical-animation-tips
lesson_title: Practical Animation Tips
heading: Use ease-in-out for elements that are already on the screen
id:
animation-theory/practical-animation-tips/use-ease-in-out-for-elements-that-are-already-on-the-screen

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

url_path:
/lessons/animation-theory/practical-animation-tips/#use-ease-in-out-for-elements-that-are-already-on-the-screen

markdown_path:
/md/sections/animation-theory/practical-animation-tips/use-ease-in-out-for-elements-that-are-already-on-the-screen.md

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


id: animation-theory/practical-animation-tips/disable-hover-effects-on-touch-devices
lesson_id: animation-theory/practical-animation-tips
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: practical-animation-tips
lesson_title: Practical Animation Tips
heading: Disable hover effects on touch devices
url_path: /lessons/animation-theory/practical-animation-tips/#disable-hover-effects-on-touch-devices
markdown_path: /md/sections/animation-theory/practical-animation-tips/disable-hover-effects-on-touch-devices.md
excerpt:
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.

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

@media (hover: hover) and (pointer: fine) {
  .card:hover {
    transform: scale(1.05);
  }
}

@media (hover: hover) and (pointer: fine) {
  .card:hover {
    transform: scale(1.05);
  }
}


id: animation-theory/practical-animation-tips/use-custom-easing-curves
lesson_id: animation-theory/practical-animation-tips
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: practical-animation-tips
lesson_title: Practical Animation Tips
heading: Use custom easing curves
url_path: /lessons/animation-theory/practical-animation-tips/#use-custom-easing-curves
markdown_path: /md/sections/animation-theory/practical-animation-tips/use-custom-easing-curves.md
excerpt:
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:

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


id: animation-theory/practical-animation-tips/use-blur-when-nothing-else-works
lesson_id: animation-theory/practical-animation-tips
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: practical-animation-tips
lesson_title: Practical Animation Tips
heading: Use blur when nothing else works
url_path: /lessons/animation-theory/practical-animation-tips/#use-blur-when-nothing-else-works
markdown_path: /md/sections/animation-theory/practical-animation-tips/use-blur-when-nothing-else-works.md
excerpt:
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.

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


id: animation-theory/practical-animation-tips/does-this-matter
lesson_id: animation-theory/practical-animation-tips
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: practical-animation-tips
lesson_title: Practical Animation Tips
heading: Does this matter?
url_path: /lessons/animation-theory/practical-animation-tips/#does-this-matter
markdown_path: /md/sections/animation-theory/practical-animation-tips/does-this-matter.md
excerpt:
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.

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


id: animation-theory/spring-animations/overview
lesson_id: animation-theory/spring-animations
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: spring-animations
lesson_title: Spring animations
heading: Overview
url_path: /lessons/animation-theory/spring-animations/#overview
markdown_path: /md/sections/animation-theory/spring-animations/overview.md
excerpt:
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.

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

.element {
  animation: scaleUp 0.3s ease-out;
}

@keyframes scaleUp {
  0% {
    transform: scale(0);
  }
  100% {
    transform: scale(1);
  }
}

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


id: animation-theory/spring-animations/how-to-craft-a-spring-animation
lesson_id: animation-theory/spring-animations
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: spring-animations
lesson_title: Spring animations
heading: How to craft a spring animation?
url_path: /lessons/animation-theory/spring-animations/#how-to-craft-a-spring-animation
markdown_path: /md/sections/animation-theory/spring-animations/how-to-craft-a-spring-animation.md
excerpt:
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.

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


id: animation-theory/spring-animations/interruptibility
lesson_id: animation-theory/spring-animations
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: spring-animations
lesson_title: Spring animations
heading: Interruptibility
url_path: /lessons/animation-theory/spring-animations/#interruptibility
markdown_path: /md/sections/animation-theory/spring-animations/interruptibility.md
excerpt:
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.

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


id: animation-theory/spring-animations/bounce
lesson_id: animation-theory/spring-animations
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: spring-animations
lesson_title: Spring animations
heading: Bounce
url_path: /lessons/animation-theory/spring-animations/#bounce
markdown_path: /md/sections/animation-theory/spring-animations/bounce.md
excerpt:
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.

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


id: animation-theory/spring-animations/should-you-use-spring-animation-for-everything
lesson_id: animation-theory/spring-animations
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: spring-animations
lesson_title: Spring animations
heading: Should you use spring animation for everything?
url_path: /lessons/animation-theory/spring-animations/#should-you-use-spring-animation-for-everything
markdown_path: /md/sections/animation-theory/spring-animations/should-you-use-spring-animation-for-everything.md
excerpt:
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.

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


id: animation-theory/spring-animations/vaul-as-example
lesson_id: animation-theory/spring-animations
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: spring-animations
lesson_title: Spring animations
heading: Vaul as example
url_path: /lessons/animation-theory/spring-animations/#vaul-as-example
markdown_path: /md/sections/animation-theory/spring-animations/vaul-as-example.md
excerpt:
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.

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


id: animation-theory/spring-animations/spring-animations-in-figma
lesson_id: animation-theory/spring-animations
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: spring-animations
lesson_title: Spring animations
heading: Spring animations in Figma
url_path: /lessons/animation-theory/spring-animations/#spring-animations-in-figma
markdown_path: /md/sections/animation-theory/spring-animations/spring-animations-in-figma.md
excerpt:
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

body:
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


id: animation-theory/spring-animations/what-s-next
lesson_id: animation-theory/spring-animations
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: spring-animations
lesson_title: Spring animations
heading: What’s next?
url_path: /lessons/animation-theory/spring-animations/#what-s-next
markdown_path: /md/sections/animation-theory/spring-animations/what-s-next.md
excerpt:
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.

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


id: animation-theory/taste/overview
lesson_id: animation-theory/taste
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: taste
lesson_title: Taste
heading: Overview
url_path: /lessons/animation-theory/taste/#overview
markdown_path: /md/sections/animation-theory/taste/overview.md
excerpt:
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.

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


id: animation-theory/taste/can-you-improve-your-taste
lesson_id: animation-theory/taste
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: taste
lesson_title: Taste
heading: Can you improve your taste?
url_path: /lessons/animation-theory/taste/#can-you-improve-your-taste
markdown_path: /md/sections/animation-theory/taste/can-you-improve-your-taste.md
excerpt:
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.

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


id: animation-theory/taste/why-does-taste-matter
lesson_id: animation-theory/taste
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: taste
lesson_title: Taste
heading: Why does taste matter?
url_path: /lessons/animation-theory/taste/#why-does-taste-matter
markdown_path: /md/sections/animation-theory/taste/why-does-taste-matter.md
excerpt:
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.

body:
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?


id: animation-theory/taste/how-to-get-better
lesson_id: animation-theory/taste
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: taste
lesson_title: Taste
heading: How to get better?
excerpt: You need to surround yourself with great work.
url_path: /lessons/animation-theory/taste/#how-to-get-better
markdown_path: /md/sections/animation-theory/taste/how-to-get-better.md
body:
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.


id: animation-theory/taste/think-about-why-you-like-something
lesson_id: animation-theory/taste
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: taste
lesson_title: Taste
heading: Think about why you like something
url_path: /lessons/animation-theory/taste/#think-about-why-you-like-something
markdown_path: /md/sections/animation-theory/taste/think-about-why-you-like-something.md
excerpt:
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.

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


id: animation-theory/taste/practice
lesson_id: animation-theory/taste
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: taste
lesson_title: Taste
heading: Practice
url_path: /lessons/animation-theory/taste/#practice
markdown_path: /md/sections/animation-theory/taste/practice.md
excerpt:
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.

body:
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:


id: animation-theory/taste/care
lesson_id: animation-theory/taste
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: taste
lesson_title: Taste
heading: Care
url_path: /lessons/animation-theory/taste/#care
markdown_path: /md/sections/animation-theory/taste/care.md
excerpt:
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.

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


id: animation-theory/taste/homework
lesson_id: animation-theory/taste
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: taste
lesson_title: Taste
heading: Homework
url_path: /lessons/animation-theory/taste/#homework
markdown_path: /md/sections/animation-theory/taste/homework.md
excerpt:
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.

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


id: animation-theory/the-easing-blueprint/overview
lesson_id: animation-theory/the-easing-blueprint
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: the-easing-blueprint
lesson_title: The Easing Blueprint
heading: Overview
url_path: /lessons/animation-theory/the-easing-blueprint/#overview
markdown_path: /md/sections/animation-theory/the-easing-blueprint/overview.md
excerpt:
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.

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


id: animation-theory/the-easing-blueprint/ease-out
lesson_id: animation-theory/the-easing-blueprint
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: the-easing-blueprint
lesson_title: The Easing Blueprint
heading: ease-out
url_path: /lessons/animation-theory/the-easing-blueprint/#ease-out
markdown_path: /md/sections/animation-theory/the-easing-blueprint/ease-out.md
excerpt:
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.

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


id: animation-theory/the-easing-blueprint/ease-in-out
lesson_id: animation-theory/the-easing-blueprint
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: the-easing-blueprint
lesson_title: The Easing Blueprint
heading: ease-in-out
url_path: /lessons/animation-theory/the-easing-blueprint/#ease-in-out
markdown_path: /md/sections/animation-theory/the-easing-blueprint/ease-in-out.md
excerpt:
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.

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


id: animation-theory/the-easing-blueprint/ease-in
lesson_id: animation-theory/the-easing-blueprint
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: the-easing-blueprint
lesson_title: The Easing Blueprint
heading: ease-in
url_path: /lessons/animation-theory/the-easing-blueprint/#ease-in
markdown_path: /md/sections/animation-theory/the-easing-blueprint/ease-in.md
excerpt:
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.

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


id: animation-theory/the-easing-blueprint/linear
lesson_id: animation-theory/the-easing-blueprint
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: the-easing-blueprint
lesson_title: The Easing Blueprint
heading: linear
url_path: /lessons/animation-theory/the-easing-blueprint/#linear
markdown_path: /md/sections/animation-theory/the-easing-blueprint/linear.md
excerpt:
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.

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


id: animation-theory/the-easing-blueprint/ease
lesson_id: animation-theory/the-easing-blueprint
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: the-easing-blueprint
lesson_title: The Easing Blueprint
heading: ease
url_path: /lessons/animation-theory/the-easing-blueprint/#ease
markdown_path: /md/sections/animation-theory/the-easing-blueprint/ease.md
excerpt:
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.

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


id: animation-theory/the-easing-blueprint/custom-easings
lesson_id: animation-theory/the-easing-blueprint
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: the-easing-blueprint
lesson_title: The Easing Blueprint
heading: Custom easings
url_path: /lessons/animation-theory/the-easing-blueprint/#custom-easings
markdown_path: /md/sections/animation-theory/the-easing-blueprint/custom-easings.md
excerpt:
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.

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


id: animation-theory/the-easing-blueprint/create-your-own-curve
lesson_id: animation-theory/the-easing-blueprint
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: the-easing-blueprint
lesson_title: The Easing Blueprint
heading: Create your own curve
url_path: /lessons/animation-theory/the-easing-blueprint/#create-your-own-curve
markdown_path: /md/sections/animation-theory/the-easing-blueprint/create-your-own-curve.md
excerpt:
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.

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


id: animation-theory/timing-and-purpose/overview
lesson_id: animation-theory/timing-and-purpose
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: timing-and-purpose
lesson_title: Timing and purpose
heading: Overview
url_path: /lessons/animation-theory/timing-and-purpose/#overview
markdown_path: /md/sections/animation-theory/timing-and-purpose/overview.md
excerpt:
When done right, animations make an interface feel predictable, faster, and more enjoyable to use. They help you and your product stand out.

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


id: animation-theory/timing-and-purpose/purposeful-animations
lesson_id: animation-theory/timing-and-purpose
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: timing-and-purpose
lesson_title: Timing and purpose
heading: Purposeful animations
url_path: /lessons/animation-theory/timing-and-purpose/#purposeful-animations
markdown_path: /md/sections/animation-theory/timing-and-purpose/purposeful-animations.md
excerpt:
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?

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


id: animation-theory/timing-and-purpose/frequency-of-use
lesson_id: animation-theory/timing-and-purpose
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: timing-and-purpose
lesson_title: Timing and purpose
heading: Frequency of use
url_path: /lessons/animation-theory/timing-and-purpose/#frequency-of-use
markdown_path: /md/sections/animation-theory/timing-and-purpose/frequency-of-use.md
excerpt:
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.

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


id: animation-theory/timing-and-purpose/perception-of-speed
lesson_id: animation-theory/timing-and-purpose
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: timing-and-purpose
lesson_title: Timing and purpose
heading: Perception of speed
url_path: /lessons/animation-theory/timing-and-purpose/#perception-of-speed
markdown_path: /md/sections/animation-theory/timing-and-purpose/perception-of-speed.md
excerpt:
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.

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


id: animation-theory/timing-and-purpose/choosing-the-right-duration
lesson_id: animation-theory/timing-and-purpose
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: timing-and-purpose
lesson_title: Timing and purpose
heading: Choosing the right duration
url_path: /lessons/animation-theory/timing-and-purpose/#choosing-the-right-duration
markdown_path: /md/sections/animation-theory/timing-and-purpose/choosing-the-right-duration.md
excerpt:
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.

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


id: animation-theory/timing-and-purpose/marketing-vs-product-animations
lesson_id: animation-theory/timing-and-purpose
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: timing-and-purpose
lesson_title: Timing and purpose
heading: Marketing vs Product animations
url_path: /lessons/animation-theory/timing-and-purpose/#marketing-vs-product-animations
markdown_path: /md/sections/animation-theory/timing-and-purpose/marketing-vs-product-animations.md
excerpt:
Most of the rules above apply to both marketing and product animations, but marketing pages give you more freedom.

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


id: animation-theory/timing-and-purpose/great-interfaces
lesson_id: animation-theory/timing-and-purpose
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: timing-and-purpose
lesson_title: Timing and purpose
heading: Great interfaces
url_path: /lessons/animation-theory/timing-and-purpose/#great-interfaces
markdown_path: /md/sections/animation-theory/timing-and-purpose/great-interfaces.md
excerpt:
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.

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


id: animation-theory/what-makes-an-animation-feel-right/what-s-the-difference
lesson_id: animation-theory/what-makes-an-animation-feel-right
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: what-makes-an-animation-feel-right
lesson_title: What makes an animation feel right?
heading: What’s the difference?
excerpt: Does this animation feel good to you?
url_path: /lessons/animation-theory/what-makes-an-animation-feel-right/#what-s-the-difference
markdown_path: /md/sections/animation-theory/what-makes-an-animation-feel-right/what-s-the-difference.md
body:
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.


id: animation-theory/what-makes-an-animation-feel-right/purpose
lesson_id: animation-theory/what-makes-an-animation-feel-right
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: what-makes-an-animation-feel-right
lesson_title: What makes an animation feel right?
heading: Purpose
url_path: /lessons/animation-theory/what-makes-an-animation-feel-right/#purpose
markdown_path: /md/sections/animation-theory/what-makes-an-animation-feel-right/purpose.md
excerpt:
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.

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


id: animation-theory/what-makes-an-animation-feel-right/taste
lesson_id: animation-theory/what-makes-an-animation-feel-right
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: what-makes-an-animation-feel-right
lesson_title: What makes an animation feel right?
heading: Taste
url_path: /lessons/animation-theory/what-makes-an-animation-feel-right/#taste
markdown_path: /md/sections/animation-theory/what-makes-an-animation-feel-right/taste.md
excerpt:
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.

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


id: animation-theory/what-makes-an-animation-feel-right/summary
lesson_id: animation-theory/what-makes-an-animation-feel-right
module_slug: animation-theory
module_title: Animation Theory
lesson_slug: what-makes-an-animation-feel-right
lesson_title: What makes an animation feel right?
heading: Summary
excerpt: Animations feel right when:
url_path: /lessons/animation-theory/what-makes-an-animation-feel-right/#summary
markdown_path: /md/sections/animation-theory/what-makes-an-animation-feel-right/summary.md
body:
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!


id: css-animations/keyframe-animations/use-keyframe-animations-when
lesson_id: css-animations/keyframe-animations
module_slug: css-animations
module_title: CSS Animations
lesson_slug: keyframe-animations
lesson_title: Keyframe Animations
heading: Use keyframe animations when:
excerpt: We’ll go in-depth on the examples later.
url_path: /lessons/css-animations/keyframe-animations/#use-keyframe-animations-when
markdown_path: /md/sections/css-animations/keyframe-animations/use-keyframe-animations-when.md
body:
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.


id: css-animations/keyframe-animations/use-css-transitions-when
lesson_id: css-animations/keyframe-animations
module_slug: css-animations
module_title: CSS Animations
lesson_slug: keyframe-animations
lesson_title: Keyframe Animations
heading: Use CSS transitions when:
excerpt: You can see more examples of CSS transitions in the previous lesson.
url_path: /lessons/css-animations/keyframe-animations/#use-css-transitions-when
markdown_path: /md/sections/css-animations/keyframe-animations/use-css-transitions-when.md
body:
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.


id: css-animations/keyframe-animations/creating-keyframe-animations
lesson_id: css-animations/keyframe-animations
module_slug: css-animations
module_title: CSS Animations
lesson_slug: keyframe-animations
lesson_title: Keyframe Animations
heading: Creating keyframe animations
url_path: /lessons/css-animations/keyframe-animations/#creating-keyframe-animations
markdown_path: /md/sections/css-animations/keyframe-animations/creating-keyframe-animations.md
excerpt:
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.

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

@keyframes fade-in {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

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

.element {
  animation: fade-in 1s ease;
}

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


id: css-animations/keyframe-animations/iteration-count
lesson_id: css-animations/keyframe-animations
module_slug: css-animations
module_title: CSS Animations
lesson_slug: keyframe-animations
lesson_title: Keyframe Animations
heading: Iteration count
url_path: /lessons/css-animations/keyframe-animations/#iteration-count
markdown_path: /md/sections/css-animations/keyframe-animations/iteration-count.md
excerpt:
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.

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


id: css-animations/keyframe-animations/multiple-steps
lesson_id: css-animations/keyframe-animations
module_slug: css-animations
module_title: CSS Animations
lesson_slug: keyframe-animations
lesson_title: Keyframe Animations
heading: Multiple steps
url_path: /lessons/css-animations/keyframe-animations/#multiple-steps
markdown_path: /md/sections/css-animations/keyframe-animations/multiple-steps.md
excerpt:
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.

body:
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:

@keyframes blink {
  0% {
    visibility: visible;
  }
  50% {
    visibility: hidden;
  }
  100% {
    visibility: visible;
  }
}

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

@keyframes blink {
  50% {
    visibility: hidden;
  }
}

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


id: css-animations/keyframe-animations/maintaining-end-state
lesson_id: css-animations/keyframe-animations
module_slug: css-animations
module_title: CSS Animations
lesson_slug: keyframe-animations
lesson_title: Keyframe Animations
heading: Maintaining end state
url_path: /lessons/css-animations/keyframe-animations/#maintaining-end-state
markdown_path: /md/sections/css-animations/keyframe-animations/maintaining-end-state.md
excerpt:
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:

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


id: css-animations/keyframe-animations/how-is-this-animation-triggered
lesson_id: css-animations/keyframe-animations
module_slug: css-animations
module_title: CSS Animations
lesson_slug: keyframe-animations
lesson_title: Keyframe Animations
heading: How is this animation triggered?
url_path: /lessons/css-animations/keyframe-animations/#how-is-this-animation-triggered
markdown_path: /md/sections/css-animations/keyframe-animations/how-is-this-animation-triggered.md
excerpt:
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.

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


id: css-animations/keyframe-animations/pausing-animations
lesson_id: css-animations/keyframe-animations
module_slug: css-animations
module_title: CSS Animations
lesson_slug: keyframe-animations
lesson_title: Keyframe Animations
heading: Pausing animations
url_path: /lessons/css-animations/keyframe-animations/#pausing-animations
markdown_path: /md/sections/css-animations/keyframe-animations/pausing-animations.md
excerpt:
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.

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

/* 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);
  }
}

/* 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);
  }
}


id: css-animations/keyframe-animations/text-reveal
lesson_id: css-animations/keyframe-animations
module_slug: css-animations
module_title: CSS Animations
lesson_slug: keyframe-animations
lesson_title: Keyframe Animations
heading: Text reveal
url_path: /lessons/css-animations/keyframe-animations/#text-reveal
markdown_path: /md/sections/css-animations/keyframe-animations/text-reveal.md
excerpt:
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:

body:
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 <h1 />. Good luck!

import "./styles.css";
import { useState } from "react";

export default function TextReveal() {
const [reset, setReset] = useState(0);

return (
  <div>
    <div key={reset}>
      <h1 className="h1">
        Animations
      </h1>
    </div>
    {/* Use this button to replay your animation */}
    <button className="button" onClick={() => setReset(reset + 1)}>
      Replay animation
    </button>
  </div>
);
}


id: css-animations/keyframe-animations/orbiting-animation
lesson_id: css-animations/keyframe-animations
module_slug: css-animations
module_title: CSS Animations
lesson_slug: keyframe-animations
lesson_title: Keyframe Animations
heading: Orbiting animation
url_path: /lessons/css-animations/keyframe-animations/#orbiting-animation
markdown_path: /md/sections/css-animations/keyframe-animations/orbiting-animation.md
excerpt:
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:

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

import "./styles.css";

export default function Orbit() {
return (
  <div>
    <div className="circle" />
    <div className="orbitingCircle" />
  </div>
);
}

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.


id: css-animations/keyframe-animations/reverse-engineering
lesson_id: css-animations/keyframe-animations
module_slug: css-animations
module_title: CSS Animations
lesson_slug: keyframe-animations
lesson_title: Keyframe Animations
heading: Reverse engineering
url_path: /lessons/css-animations/keyframe-animations/#reverse-engineering
markdown_path: /md/sections/css-animations/keyframe-animations/reverse-engineering.md
excerpt:
In the previous module, we talked about surrounding yourself with great work to develop taste. Remember this quote?

body:
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!

import "./styles.css";

export default function Orbit() {
return (
  <div className="coin">
    <CoinIcon />
  </div>
);
}


function CoinIcon() {
return (
  <svg
    viewBox="0 0 718 718"
    fill="none"
    xmlns="http://www.w3.org/2000/svg"
  >
    <path d="M170 119H548V599H170V119Z" fill="#F8A400" />
    <path
      fillRule="evenodd"
      clipRule="evenodd"
      d="M0 359C0 160.73 160.73 0 359 0C557.269 0 718 160.73 718 359C718 557.269 557.269 718 359 718C160.73 718 0 557.269 0 359ZM359 129.14C373.87 129.14 385.925 141.195 385.925 156.065V187.06C416.681 193.03 443.789 209.116 460.723 232.531C469.436 244.582 466.729 261.414 454.681 270.127C442.629 278.84 425.796 276.135 417.083 264.085C406.923 250.034 385.821 238.336 359 238.336H348.751C312.42 238.336 293.684 260.945 293.684 277.006V279.822C293.684 292.136 302.644 307.458 324.026 316.013L413.97 351.989C450.531 366.611 478.166 398.669 478.166 438.177C478.166 489.148 434.641 524.215 385.925 531.916V561.936C385.925 576.805 373.87 588.861 359 588.861C344.13 588.861 332.075 576.805 332.075 561.936V530.94C301.319 524.969 274.21 508.883 257.279 485.469C248.566 473.417 251.271 456.587 263.321 447.874C275.371 439.161 292.204 441.864 300.917 453.916C311.077 467.967 332.179 479.664 359 479.664H365.728C404.004 479.664 424.316 455.794 424.316 438.177C424.316 425.864 415.356 410.542 393.974 401.987L304.03 366.011C267.471 351.389 239.832 319.331 239.832 279.822V277.006C239.832 226.408 283.852 192.125 332.075 185.613V156.065C332.075 141.195 344.13 129.14 359 129.14Z"
      fill="#FFCD6C"
    />
  </svg>
)
}


id: css-animations/the-beauty-of-css-animations/overview
lesson_id: css-animations/the-beauty-of-css-animations
module_slug: css-animations
module_title: CSS Animations
lesson_slug: the-beauty-of-css-animations
lesson_title: The beauty of CSS animations
heading: Overview
url_path: /lessons/css-animations/the-beauty-of-css-animations/#overview
markdown_path: /md/sections/css-animations/the-beauty-of-css-animations/overview.md
excerpt:
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.

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


id: css-animations/the-beauty-of-css-animations/right-tool-for-the-job
lesson_id: css-animations/the-beauty-of-css-animations
module_slug: css-animations
module_title: CSS Animations
lesson_slug: the-beauty-of-css-animations
lesson_title: The beauty of CSS animations
heading: Right tool for the job
url_path: /lessons/css-animations/the-beauty-of-css-animations/#right-tool-for-the-job
markdown_path: /md/sections/css-animations/the-beauty-of-css-animations/right-tool-for-the-job.md
excerpt:
I agree that you don’t need Framer Motion for a simple hover animation or enter animations, but that’s pretty much it.

body:
I agree that you don’t need Framer Motion for a simple hover animation or enter animations, but that’s pretty much it.


id: css-animations/the-beauty-of-css-animations/disclaimer
lesson_id: css-animations/the-beauty-of-css-animations
module_slug: css-animations
module_title: CSS Animations
lesson_slug: the-beauty-of-css-animations
lesson_title: The beauty of CSS animations
heading: Disclaimer
url_path: /lessons/css-animations/the-beauty-of-css-animations/#disclaimer
markdown_path: /md/sections/css-animations/the-beauty-of-css-animations/disclaimer.md
excerpt:
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.

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


id: css-animations/the-beauty-of-css-animations/performance
lesson_id: css-animations/the-beauty-of-css-animations
module_slug: css-animations
module_title: CSS Animations
lesson_slug: the-beauty-of-css-animations
lesson_title: The beauty of CSS animations
heading: Performance
url_path: /lessons/css-animations/the-beauty-of-css-animations/#performance
markdown_path: /md/sections/css-animations/the-beauty-of-css-animations/performance.md
excerpt:
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.

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


id: css-animations/the-beauty-of-css-animations/when-to-use-css-animations
lesson_id: css-animations/the-beauty-of-css-animations
module_slug: css-animations
module_title: CSS Animations
lesson_slug: the-beauty-of-css-animations
lesson_title: The beauty of CSS animations
heading: When to use CSS animations
excerpt: Use CSS animations when:
url_path: /lessons/css-animations/the-beauty-of-css-animations/#when-to-use-css-animations
markdown_path: /md/sections/css-animations/the-beauty-of-css-animations/when-to-use-css-animations.md
body:
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.


id: css-animations/the-magic-of-clip-path/overview
lesson_id: css-animations/the-magic-of-clip-path
module_slug: css-animations
module_title: CSS Animations
lesson_slug: the-magic-of-clip-path
lesson_title: The Magic of Clip Path
heading: Overview
url_path: /lessons/css-animations/the-magic-of-clip-path/#overview
markdown_path: /md/sections/css-animations/the-magic-of-clip-path/overview.md
excerpt:
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?

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


id: css-animations/the-magic-of-clip-path/the-basics
lesson_id: css-animations/the-magic-of-clip-path
module_slug: css-animations
module_title: CSS Animations
lesson_slug: the-magic-of-clip-path
lesson_title: The Magic of Clip Path
heading: The Basics
url_path: /lessons/css-animations/the-magic-of-clip-path/#the-basics
markdown_path: /md/sections/css-animations/the-magic-of-clip-path/the-basics.md
excerpt:
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.

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

.circle {
  clip-path: circle(50% at 50% 50%);
}

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


id: css-animations/the-magic-of-clip-path/positioning
lesson_id: css-animations/the-magic-of-clip-path
module_slug: css-animations
module_title: CSS Animations
lesson_slug: the-magic-of-clip-path
lesson_title: The Magic of Clip Path
heading: Positioning
url_path: /lessons/css-animations/the-magic-of-clip-path/#positioning
markdown_path: /md/sections/css-animations/the-magic-of-clip-path/positioning.md
excerpt:
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.

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


id: css-animations/the-magic-of-clip-path/comparison-sliders
lesson_id: css-animations/the-magic-of-clip-path
module_slug: css-animations
module_title: CSS Animations
lesson_slug: the-magic-of-clip-path
lesson_title: The Magic of Clip Path
heading: Comparison Sliders
url_path: /lessons/css-animations/the-magic-of-clip-path/#comparison-sliders
markdown_path: /md/sections/css-animations/the-magic-of-clip-path/comparison-sliders.md
excerpt:
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.

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


id: css-animations/the-magic-of-clip-path/animating-images
lesson_id: css-animations/the-magic-of-clip-path
module_slug: css-animations
module_title: CSS Animations
lesson_slug: the-magic-of-clip-path
lesson_title: The Magic of Clip Path
heading: Animating images
url_path: /lessons/css-animations/the-magic-of-clip-path/#animating-images
markdown_path: /md/sections/css-animations/the-magic-of-clip-path/animating-images.md
excerpt:
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.

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

.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);
  }
}

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


id: css-animations/the-magic-of-clip-path/scroll-animations
lesson_id: css-animations/the-magic-of-clip-path
module_slug: css-animations
module_title: CSS Animations
lesson_slug: the-magic-of-clip-path
lesson_title: The Magic of Clip Path
heading: Scroll animations
url_path: /lessons/css-animations/the-magic-of-clip-path/#scroll-animations
markdown_path: /md/sections/css-animations/the-magic-of-clip-path/scroll-animations.md
excerpt:
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?

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

"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 (
  <>
  <h1>scroll down</h1>
    <img
      className="image-reveal"
      alt="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."
      src="https://animations.dev/css-animations/raycast.jpg"
      height={430}
      ref={ref}
      width={644}
    />
  </>
);
}

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.


id: css-animations/the-magic-of-clip-path/tabs-transition
lesson_id: css-animations/the-magic-of-clip-path
module_slug: css-animations
module_title: CSS Animations
lesson_slug: the-magic-of-clip-path
lesson_title: The Magic of Clip Path
heading: Tabs transition
url_path: /lessons/css-animations/the-magic-of-clip-path/#tabs-transition
markdown_path: /md/sections/css-animations/the-magic-of-clip-path/tabs-transition.md
excerpt:
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.

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

"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 (
  <div className="wrapper">
    <ul className="list">
      {TABS.map((tab) => (
        <li key={tab.name}>
          <button
            ref={activeTab === tab.name ? activeTabElementRef : null}
            data-tab={tab.name}
            onClick={() => {
              setActiveTab(tab.name);
            }}
            className="button"
          >
            {tab.icon}
            {tab.name}
          </button>
        </li>
      ))}
    </ul>

    <div aria-hidden className="clip-path-container" ref={containerRef}>
      <ul className="list list-overlay">
        {TABS.map((tab) => (
          <li key={tab.name}>
            <button
              data-tab={tab.name}
              onClick={() => {
                setActiveTab(tab.name);
              }}
              className="button-overlay button"
			tabIndex={-1}
            >
              {tab.icon}
              {tab.name}
            </button>
          </li>
        ))}
      </ul>
    </div>
  </div>
);
}

const TABS = [
{
  name: "Payments",
  icon: (
    <svg
      aria-hidden="true"
      width="16"
      height="16"
      viewBox="0 0 16 16"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path
        fillRule="evenodd"
        clipRule="evenodd"
        fill="currentColor"
        d="M0 3.884c0-.8.545-1.476 1.306-1.68l.018-.004L10.552.213c.15-.038.3-.055.448-.055.927.006 1.75.733 1.75 1.74V4.5h.75A2.5 2.5 0 0 1 16 7v6.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 13.5V3.884ZM10.913 1.67c.199-.052.337.09.337.23v2.6H2.5c-.356 0-.694.074-1 .208v-.824c0-.092.059-.189.181-.227l9.216-1.984.016-.004ZM1.5 7v6.5a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1h-11a1 1 0 0 0-1 1Z"
      ></path>
      <path
        fillRule="evenodd"
        clipRule="evenodd"
        fill="currentColor"
        d="M10.897 1.673 1.681 3.657c-.122.038-.181.135-.181.227v.824a2.492 2.492 0 0 1 1-.208h8.75V1.898c0-.14-.138-.281-.337-.23m0 0-.016.005Zm-9.59.532 9.23-1.987c.15-.038.3-.055.448-.055.927.006 1.75.733 1.75 1.74V4.5h.75A2.5 2.5 0 0 1 16 7v6.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 13.5V3.884c0-.8.545-1.476 1.306-1.68l.018-.004ZM1.5 13.5V7a1 1 0 0 1 1-1h11a1 1 0 0 1 1 1v6.5a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1ZM13 10.25c0 .688-.563 1.25-1.25 1.25-.688 0-1.25-.55-1.25-1.25 0-.688.563-1.25 1.25-1.25.688 0 1.25.562 1.25 1.25Z"
      ></path>
    </svg>
  ),
},
{
  name: "Balances",
  icon: (
    <svg
      data-testid="primary-nav-item-icon"
      aria-hidden="true"
      width="16"
      height="16"
      viewBox="0 0 16 16"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path
        fill="currentColor"
        d="M1 2a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 1 2Zm0 8a.75.75 0 0 1 .75-.75h5a.75.75 0 0 1 0 1.5h-5A.75.75 0 0 1 1 10Zm2.25-4.75a.75.75 0 0 0 0 1.5h7.5a.75.75 0 0 0 0-1.5h-7.5ZM2.5 14a.75.75 0 0 1 .75-.75h4a.75.75 0 0 1 0 1.5h-4A.75.75 0 0 1 2.5 14Z"
      ></path>
      <path
        fillRule="evenodd"
        clipRule="evenodd"
        fill="currentColor"
        d="M16 11.5a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Zm-1.5 0a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z"
      ></path>
    </svg>
  ),
},
{
  name: "Customers",
  icon: (
    <svg
      aria-hidden="true"
      width="16"
      height="16"
      viewBox="0 0 16 16"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path
        fillRule="evenodd"
        clipRule="evenodd"
        fill="currentColor"
        d="M2.5 14.4h11a.4.4 0 0 0 .4-.4 3.4 3.4 0 0 0-3.4-3.4h-5A3.4 3.4 0 0 0 2.1 14c0 .22.18.4.4.4Zm0 1.6h11a2 2 0 0 0 2-2 5 5 0 0 0-5-5h-5a5 5 0 0 0-5 5 2 2 0 0 0 2 2ZM8 6.4a2.4 2.4 0 1 0 0-4.8 2.4 2.4 0 0 0 0 4.8ZM8 8a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z"
      ></path>
    </svg>
  ),
},
{
  name: "Billing",
  icon: (
    <svg
      aria-hidden="true"
      width="16"
      height="16"
      viewBox="0 0 16 16"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path
        fill="currentColor"
        d="M0 2.25A2.25 2.25 0 0 1 2.25 0h7.5A2.25 2.25 0 0 1 12 2.25v6a.75.75 0 0 1-1.5 0v-6a.75.75 0 0 0-.75-.75h-7.5a.75.75 0 0 0-.75.75v10.851a.192.192 0 0 0 .277.172l.888-.444a.75.75 0 1 1 .67 1.342l-.887.443A1.69 1.69 0 0 1 0 13.101V2.25Z"
      ></path>
      <path
        fill="currentColor"
        d="M5 10.7a.7.7 0 0 1 .7-.7h4.6a.7.7 0 1 1 0 1.4H7.36l.136.237c.098.17.193.336.284.491.283.483.554.907.855 1.263.572.675 1.249 1.109 2.365 1.109 1.18 0 2.038-.423 2.604-1.039.576-.626.896-1.5.896-2.461 0-.99-.42-1.567-.807-1.998a.75.75 0 1 1 1.115-1.004C15.319 8.568 16 9.49 16 11c0 1.288-.43 2.54-1.292 3.476C13.838 15.423 12.57 16 11 16c-1.634 0-2.706-.691-3.51-1.64-.386-.457-.71-.971-1.004-1.472L6.4 12.74v2.56a.7.7 0 1 1-1.4 0v-4.6ZM2.95 4.25a.75.75 0 0 1 .75-.75h2a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1-.75-.75ZM3.7 6.5a.75.75 0 0 0 0 1.5h4.6a.75.75 0 0 0 0-1.5H3.7Z"
      ></path>
    </svg>
  ),
},
];


id: css-animations/the-magic-of-clip-path/going-a-step-further
lesson_id: css-animations/the-magic-of-clip-path
module_slug: css-animations
module_title: CSS Animations
lesson_slug: the-magic-of-clip-path
lesson_title: The Magic of Clip Path
heading: Going a step further
url_path: /lessons/css-animations/the-magic-of-clip-path/#going-a-step-further
markdown_path: /md/sections/css-animations/the-magic-of-clip-path/going-a-step-further.md
excerpt:
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.

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


id: css-animations/the-magic-of-clip-path/clip-path-is-everywhere
lesson_id: css-animations/the-magic-of-clip-path
module_slug: css-animations
module_title: CSS Animations
lesson_slug: the-magic-of-clip-path
lesson_title: The Magic of Clip Path
heading: Clip Path is everywhere
url_path: /lessons/css-animations/the-magic-of-clip-path/#clip-path-is-everywhere
markdown_path: /md/sections/css-animations/the-magic-of-clip-path/clip-path-is-everywhere.md
excerpt:
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.

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


id: css-animations/the-magic-of-clip-path/hold-to-delete
lesson_id: css-animations/the-magic-of-clip-path
module_slug: css-animations
module_title: CSS Animations
lesson_slug: the-magic-of-clip-path
lesson_title: The Magic of Clip Path
heading: Hold to delete
url_path: /lessons/css-animations/the-magic-of-clip-path/#hold-to-delete
markdown_path: /md/sections/css-animations/the-magic-of-clip-path/hold-to-delete.md
excerpt:
To solidify what we just learned, let’s implement a hold to delete animation as it’s a clever use case for clip-path.

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

"use client";

export default function ClipPathButton() {
return (
  <button className="button">
    <svg height="16" strokeLinejoin="round" viewBox="0 0 16 16" width="16">
      <path
        fillRule="evenodd"
        clipRule="evenodd"
        d="M6.75 2.75C6.75 2.05964 7.30964 1.5 8 1.5C8.69036 1.5 9.25 2.05964 9.25 2.75V3H6.75V2.75ZM5.25 3V2.75C5.25 1.23122 6.48122 0 8 0C9.51878 0 10.75 1.23122 10.75 2.75V3H12.9201H14.25H15V4.5H14.25H13.8846L13.1776 13.6917C13.0774 14.9942 11.9913 16 10.6849 16H5.31508C4.00874 16 2.92263 14.9942 2.82244 13.6917L2.11538 4.5H1.75H1V3H1.75H3.07988H5.25ZM4.31802 13.5767L3.61982 4.5H12.3802L11.682 13.5767C11.6419 14.0977 11.2075 14.5 10.6849 14.5H5.31508C4.79254 14.5 4.3581 14.0977 4.31802 13.5767Z"
        fill="currentColor"
      />
    </svg>
    Hold to Delete
  </button>
);
}


id: css-animations/transforms/overview
lesson_id: css-animations/transforms
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transforms
lesson_title: Transforms
heading: Overview
url_path: /lessons/css-animations/transforms/#overview
markdown_path: /md/sections/css-animations/transforms/overview.md
excerpt:
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.

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


id: css-animations/transforms/translation
lesson_id: css-animations/transforms
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transforms
lesson_title: Transforms
heading: Translation
url_path: /lessons/css-animations/transforms/#translation
markdown_path: /md/sections/css-animations/transforms/translation.md
excerpt:
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.

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


id: css-animations/transforms/scale
lesson_id: css-animations/transforms
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transforms
lesson_title: Transforms
heading: Scale
url_path: /lessons/css-animations/transforms/#scale
markdown_path: /md/sections/css-animations/transforms/scale.md
excerpt:
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.

body:
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:

button {
  transition: transform 150ms ease;
}

button:active {
  transform: scale(0.97);
}

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.


id: css-animations/transforms/rotate
lesson_id: css-animations/transforms
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transforms
lesson_title: Transforms
heading: Rotate
url_path: /lessons/css-animations/transforms/#rotate
markdown_path: /md/sections/css-animations/transforms/rotate.md
excerpt:
Used less often than the two previous functions, but as the name suggests, rotate allows us to rotate an element.

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


id: css-animations/transforms/3d-transforms
lesson_id: css-animations/transforms
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transforms
lesson_title: Transforms
heading: 3D Transforms
url_path: /lessons/css-animations/transforms/#3d-transforms
markdown_path: /md/sections/css-animations/transforms/3d-transforms.md
excerpt:
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:

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


id: css-animations/transforms/3d-transforms-in-action
lesson_id: css-animations/transforms
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transforms
lesson_title: Transforms
heading: 3D Transforms in action
url_path: /lessons/css-animations/transforms/#3d-transforms-in-action
markdown_path: /md/sections/css-animations/transforms/3d-transforms-in-action.md
excerpt:
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.

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


id: css-animations/transforms/transform-origin
lesson_id: css-animations/transforms
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transforms
lesson_title: Transforms
heading: Transform origin
url_path: /lessons/css-animations/transforms/#transform-origin
markdown_path: /md/sections/css-animations/transforms/transform-origin.md
excerpt:
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.

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


id: css-animations/transforms/order-of-transforms
lesson_id: css-animations/transforms
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transforms
lesson_title: Transforms
heading: Order of transforms
url_path: /lessons/css-animations/transforms/#order-of-transforms
markdown_path: /md/sections/css-animations/transforms/order-of-transforms.md
excerpt:
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.

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


id: css-animations/transforms/exercise
lesson_id: css-animations/transforms
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transforms
lesson_title: Transforms
heading: Exercise
url_path: /lessons/css-animations/transforms/#exercise
markdown_path: /md/sections/css-animations/transforms/exercise.md
excerpt:
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!

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

import "./styles.css";

const LENGTH = 3;

export default function StackedComponent() {
return (
  <div className="wrapper">
    {new Array(LENGTH).fill(0).map((_, i) => (
      <div className="card" key={i} />
    ))}
  </div>
);
}


id: css-animations/transitions/overview
lesson_id: css-animations/transitions
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transitions
lesson_title: Transitions
heading: Overview
url_path: /lessons/css-animations/transitions/#overview
markdown_path: /md/sections/css-animations/transitions/overview.md
excerpt:
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.

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


id: css-animations/transitions/what-s-interpolation
lesson_id: css-animations/transitions
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transitions
lesson_title: Transitions
heading: What’s interpolation?
url_path: /lessons/css-animations/transitions/#what-s-interpolation
markdown_path: /md/sections/css-animations/transitions/what-s-interpolation.md
excerpt:
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.

body:
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:

.button {
  /* Transition transform over 200ms with ease as our timing function and a delay of 100ms */
  transition: transform 200ms ease 100ms;
}

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

.box {
  /* We can optionally add a delay after the timing-function */
  transition: transform 0.2s ease;
}

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


id: css-animations/transitions/how-i-use-css-transitions
lesson_id: css-animations/transitions
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transitions
lesson_title: Transitions
heading: How I use CSS transitions
url_path: /lessons/css-animations/transitions/#how-i-use-css-transitions
markdown_path: /md/sections/css-animations/transitions/how-i-use-css-transitions.md
excerpt:
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:

body:
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:

.box {
  /* Or any other property that you want to animate */
  transition: transform 0.2s ease;
}

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

/* 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;
}

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


id: css-animations/transitions/practice
lesson_id: css-animations/transitions
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transitions
lesson_title: Transitions
heading: Practice
url_path: /lessons/css-animations/transitions/#practice
markdown_path: /md/sections/css-animations/transitions/practice.md
excerpt:
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.

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


id: css-animations/transitions/simple-transform
lesson_id: css-animations/transitions
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transitions
lesson_title: Transitions
heading: Simple Transform
url_path: /lessons/css-animations/transitions/#simple-transform
markdown_path: /md/sections/css-animations/transitions/simple-transform.md
excerpt:
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:

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

import "./styles.css";

export default function SimpleTransformTransition() {
return (
   <div className="box" />
);
}


id: css-animations/transitions/card-hover
lesson_id: css-animations/transitions
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transitions
lesson_title: Transitions
heading: Card Hover
url_path: /lessons/css-animations/transitions/#card-hover
markdown_path: /md/sections/css-animations/transitions/card-hover.md
excerpt:
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:

body:
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:


id: css-animations/transitions/project-name
lesson_id: css-animations/transitions
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transitions
lesson_title: Transitions
heading: Project name
excerpt: Project description
url_path: /lessons/css-animations/transitions/#project-name
markdown_path: /md/sections/css-animations/transitions/project-name.md
body:
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.

import "./styles.css";

export default function CardHover() {
return (
   <a href="#" className="card">
      <div className="card-description">
        <h3 className="card-title">Project name</h3>
        <p className="card-subtitle">Project description</p>
        <svg
          width="11"
          height="11"
          viewBox="0 0 11 11"
          fill="none"
          className="card-icon"
        >
          <path
            fillRule="evenodd"
            clipRule="evenodd"
            d="M6.33333 0.4375C6.33333 0.195877 6.52922 0 6.77083 0H10.5625C10.8041 0 11 0.195877 11 0.4375V4.22917C11 4.47078 10.8041 4.66667 10.5625 4.66667C10.3209 4.66667 10.125 4.47078 10.125 4.22917V1.49372L7.08017 4.53851C6.90932 4.70937 6.63235 4.70937 6.46149 4.53851C6.29063 4.36765 6.29063 4.09068 6.46149 3.91981L9.50626 0.875H6.77083C6.52922 0.875 6.33333 0.679122 6.33333 0.4375ZM0.5 6.27083C0.5 6.02922 0.695877 5.83333 0.9375 5.83333C1.17912 5.83333 1.375 6.02922 1.375 6.27083V9.00626L4.41981 5.96149C4.59068 5.79063 4.86765 5.79063 5.03851 5.96149C5.20937 6.13235 5.20937 6.40932 5.03851 6.58017L1.99372 9.625H4.72917C4.97078 9.625 5.16667 9.82088 5.16667 10.0625C5.16667 10.3041 4.97078 10.5 4.72917 10.5H0.9375C0.695877 10.5 0.5 10.3041 0.5 10.0625V6.27083Z"
            fill="#58585F"
          />
        </svg>
      </div>
    </a>
);
}


id: css-animations/transitions/download-arrow
lesson_id: css-animations/transitions
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transitions
lesson_title: Transitions
heading: Download Arrow
url_path: /lessons/css-animations/transitions/#download-arrow
markdown_path: /md/sections/css-animations/transitions/download-arrow.md
excerpt:
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.

body:
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!

import "./styles.css";

export default function DownloadArrow() {
return (
   <button aria-label="Download PDF" className="download-button">
      {ArrowDown}
    </button>
);
}

const ArrowDown = (
<svg
  width="18"
  height="18"
  viewBox="0 0 24 24"
  fill="none"
  xmlns="http://www.w3.org/2000/svg"
>
  <path
    d="M18.25 14L12 20.25L5.75 14M12 19.5V3.75"
    stroke="black"
    strokeWidth="2"
    strokeLinecap="round"
    strokeLinejoin="round"
  />
</svg>
);


id: css-animations/transitions/toast-component
lesson_id: css-animations/transitions
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transitions
lesson_title: Transitions
heading: Toast component
url_path: /lessons/css-animations/transitions/#toast-component
markdown_path: /md/sections/css-animations/transitions/toast-component.md
excerpt:
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?

body:
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 <Toaster /> 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.

{Array.from({ length: toasts }).map((_, i) => (
  <Toast key={i} />
))}

{Array.from({ length: toasts }).map((_, i) => (
  <Toast key={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.

import "./styles.css";
import { useState } from "react";

export default function Toaster() {
const [toasts, setToasts] = useState(0);

return (
  <div className="wrapper">
    <div className="toaster">
      {Array.from({ length: toasts }).map((_, i) => (
        <Toast key={i} />
      ))}
    </div>
    <button
      className="button"
      onClick={() => {
        setToasts(toasts + 1);
      }}
    >
      Add toast
    </button>
  </div>
);
}

function Toast() {
return (
  <div className="toast">
    <span className="title">Event Created </span>
    <span className="description">Monday, January 3rd at 6:00pm</span>
  </div>
);
}


id: css-animations/transitions/disabling-hover-effects
lesson_id: css-animations/transitions
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transitions
lesson_title: Transitions
heading: Disabling hover effects
url_path: /lessons/css-animations/transitions/#disabling-hover-effects
markdown_path: /md/sections/css-animations/transitions/disabling-hover-effects.md
excerpt:
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.

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

@media (hover: hover) and (pointer: fine) {
  .card:hover {
    background: blue;
  }
}

@media (hover: hover) and (pointer: fine) {
  .card:hover {
    background: blue;
  }
}

You can enable this in Tailwind as well in your tailwind config:

// tailwind.config.js
module.exports = {
  future: {
    hoverOnlyWhenSupported: true,
  },
  // ...
}

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


id: css-animations/transitions/hover-in-tailwind-v4
lesson_id: css-animations/transitions
module_slug: css-animations
module_title: CSS Animations
lesson_slug: transitions
lesson_title: Transitions
heading: Hover in Tailwind v4
url_path: /lessons/css-animations/transitions/#hover-in-tailwind-v4
markdown_path: /md/sections/css-animations/transitions/hover-in-tailwind-v4.md
excerpt:
If you are using Tailwind v4, you don’t even have to worry about it. Hovers are disabled on touch devices by default.

body:
If you are using Tailwind v4, you don’t even have to worry about it. Hovers are disabled on touch devices by default.


id: dynamic-island/morph-effect/apple-s-approach
lesson_id: dynamic-island/morph-effect
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: morph-effect
lesson_title: Morph effect
heading: Apple’s approach
url_path: /lessons/dynamic-island/morph-effect/#apple-s-approach
markdown_path: /md/sections/dynamic-island/morph-effect/apple-s-approach.md
excerpt:
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

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


id: dynamic-island/morph-effect/the-solution
lesson_id: dynamic-island/morph-effect
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: morph-effect
lesson_title: Morph effect
heading: The solution
url_path: /lessons/dynamic-island/morph-effect/#the-solution
markdown_path: /md/sections/dynamic-island/morph-effect/the-solution.md
excerpt:
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.

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

<motion.div
  layout
  transition={{
    type: "spring",
    bounce,
  }}
  style={{ borderRadius: 32 }}
  className="mx-auto w-fit min-w-[100px] overflow-hidden rounded-full bg-black"
>
  // This is the active view which is always visible
  <motion.div
    ref={activeStateWrapperRef}
    transition={{
      type: "spring",
      bounce,
    }}
    initial={{
      scale: 0.9,
      opacity: 0,
      filter: "blur(5px)",
      originX: 0.5,
      originY: 0.5,
    }}
    animate={{
      scale: 1,
      opacity: 1,
      filter: "blur(0px)",
      originX: 0.5,
      originY: 0.5,
      transition: {
        delay: 0.05,
      },
    }}
    key={view}
  >
    {content}
  </motion.div>
</motion.div>

<div className="pointer-events-none absolute left-1/2 top-0 flex h-[200px] w-[300px] -translate-x-1/2 items-start justify-center">
  <AnimatePresence mode="popLayout" custom={exitTransition}>
    // This shows only when exiting
    <motion.div
      initial={{ opacity: 0 }}
      exit="exit"
      variants={variants}
      key={view}
    >
      {content}
    </motion.div>
  </AnimatePresence>
</div>

<motion.div
  layout
  transition={{
    type: "spring",
    bounce,
  }}
  style={{ borderRadius: 32 }}
  className="mx-auto w-fit min-w-[100px] overflow-hidden rounded-full bg-black"
>
  // This is the active view which is always visible
  <motion.div
    ref={activeStateWrapperRef}
    transition={{
      type: "spring",
      bounce,
    }}
    initial={{
      scale: 0.9,
      opacity: 0,
      filter: "blur(5px)",
      originX: 0.5,
      originY: 0.5,
    }}
    animate={{
      scale: 1,
      opacity: 1,
      filter: "blur(0px)",
      originX: 0.5,
      originY: 0.5,
      transition: {
        delay: 0.05,
      },
    }}
    key={view}
  >
    {content}
  </motion.div>
</motion.div>

<div className="pointer-events-none absolute left-1/2 top-0 flex h-[200px] w-[300px] -translate-x-1/2 items-start justify-center">
  <AnimatePresence mode="popLayout" custom={exitTransition}>
    // This shows only when exiting
    <motion.div
      initial={{ opacity: 0 }}
      exit="exit"
      variants={variants}
      key={view}
    >
      {content}
    </motion.div>
  </AnimatePresence>
</div>

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:

const variants = {
  exit: (transition) => {
    return {
      ...transition,
      opacity: [1, 0],
      filter: "blur(4px)",
    };
  },
};

const variants = {
  exit: (transition) => {
    return {
      ...transition,
      opacity: [1, 0],
      filter: "blur(4px)",
    };
  },
};

We start with opacity 1, which makes it visible again.


id: dynamic-island/morph-effect/custom-exit-animation
lesson_id: dynamic-island/morph-effect
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: morph-effect
lesson_title: Morph effect
heading: Custom exit animation
url_path: /lessons/dynamic-island/morph-effect/#custom-exit-animation
markdown_path: /md/sections/dynamic-island/morph-effect/custom-exit-animation.md
excerpt:
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.

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

// 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,
  };
}

// 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:

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


id: dynamic-island/morph-effect/custom-bounce
lesson_id: dynamic-island/morph-effect
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: morph-effect
lesson_title: Morph effect
heading: Custom bounce
url_path: /lessons/dynamic-island/morph-effect/#custom-bounce
markdown_path: /md/sections/dynamic-island/morph-effect/custom-bounce.md
excerpt:
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.

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

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,
};

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.

<motion.div
  transition={{
    type: "spring",
    bounce: BOUNCE_VARIANTS[variantKey],
  }}
/>

<AnimatePresence
  mode="popLayout"
  custom={ANIMATION_VARIANTS[variantKey]}
>
  // ...
</AnimatePresence>

<motion.div
  transition={{
    type: "spring",
    bounce: BOUNCE_VARIANTS[variantKey],
  }}
/>

<AnimatePresence
  mode="popLayout"
  custom={ANIMATION_VARIANTS[variantKey]}
>
  // ...
</AnimatePresence>


id: dynamic-island/morph-effect/the-result
lesson_id: dynamic-island/morph-effect
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: morph-effect
lesson_title: Morph effect
heading: The result
url_path: /lessons/dynamic-island/morph-effect/#the-result
markdown_path: /md/sections/dynamic-island/morph-effect/the-result.md
excerpt:
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.

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

"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 <Ring />;
    case "timer":
      return <Timer />;
    case "idle":
      return <div className="h-7" />;
  }
}, [view]);

return (
  <div className="h-[200px]">
    <div className="relative flex h-full w-full flex-col justify-between">
      <motion.div
        layout
        transition={{
          type: "spring",
          bounce: BOUNCE_VARIANTS[variantKey],
        }}
        style={{ borderRadius: 32 }}
        className="mx-auto w-fit min-w-[100px] overflow-hidden rounded-full bg-black"
      >
        <motion.div
          transition={{
            type: "spring",
            bounce: BOUNCE_VARIANTS[variantKey],
          }}
          initial={{
            scale: 0.9,
            opacity: 0,
            filter: "blur(5px)",
            originX: 0.5,
            originY: 0.5,
          }}
          animate={{
            scale: 1,
            opacity: 1,
            filter: "blur(0px)",
            originX: 0.5,
            originY: 0.5,
            transition: {
              delay: 0.05,
            },
          }}
          key={view}
        >
          {content}
        </motion.div>
      </motion.div>

      <div className="pointer-events-none absolute left-1/2 top-0 flex h-[200px] w-[300px] -translate-x-1/2 items-start justify-center">
        <AnimatePresence
          mode="popLayout"
          custom={ANIMATION_VARIANTS[variantKey]}
        >
          <motion.div
            initial={{ opacity: 0 }}
            exit="exit"
            variants={variants}
            key={view}
          >
            {content}
          </motion.div>
        </AnimatePresence>
      </div>
      <div className="flex w-full justify-center gap-4">
        {["idle", "ring", "timer"].map((v) => (
          <button
            type="button"
            className="rounded-full capitalize w-32 h-10 bg-white px-2.5 py-1.5 text-sm font-medium text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300/50 hover:bg-gray-50"
            onClick={() => {
              setView(v);
              setVariantKey(`${view}-${v}`);
            }}
            key={v}
          >
            {v}
          </button>
        ))}
      </div>
    </div>
  </div>
);
}

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,
};


id: dynamic-island/ring-view/overview
lesson_id: dynamic-island/ring-view
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: ring-view
lesson_title: Ring view
heading: Overview
url_path: /lessons/dynamic-island/ring-view/#overview
markdown_path: /md/sections/dynamic-island/ring-view/overview.md
excerpt:
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.

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


id: dynamic-island/ring-view/starter-code
lesson_id: dynamic-island/ring-view
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: ring-view
lesson_title: Ring view
heading: Starter code
url_path: /lessons/dynamic-island/ring-view/#starter-code
markdown_path: /md/sections/dynamic-island/ring-view/starter-code.md
excerpt:
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.

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

const content = useMemo(() => {
  switch (view) {
    case "ring":
      return <Ring />;
    case "idle":
      return <div className="h-7" />;
  }
}, [view]);

const content = useMemo(() => {
  switch (view) {
    case "ring":
      return <Ring />;
    case "idle":
      return <div className="h-7" />;
  }
}, [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.

"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 <Ring />;
    case "idle":
      return <div className="h-7" />;
  }
}, [view]);

return (
  <div>
    <div className="flex h-[160px] justify-center">
      <div className="h-fit min-w-[100px] overflow-hidden rounded-full bg-black">
        {content}
      </div>
    </div>
    <div className="flex justify-center gap-4">
      <button
        type="button"
        className="rounded-full w-32 h-10 bg-white px-2.5 py-1.5 text-sm font-medium text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
        onClick={() => setView("idle")}
      >
        Idle
      </button>
      <button
        type="button"
        className="rounded-full w-32 h-10 bg-white px-2.5 py-1.5 text-sm font-medium text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
        onClick={() => setView("ring")}
      >
        Ring
      </button>
    </div>
  </div>
);
}


id: dynamic-island/ring-view/what-do-we-animate
lesson_id: dynamic-island/ring-view
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: ring-view
lesson_title: Ring view
heading: What do we animate?
url_path: /lessons/dynamic-island/ring-view/#what-do-we-animate
markdown_path: /md/sections/dynamic-island/ring-view/what-do-we-animate.md
excerpt:
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.

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


id: dynamic-island/ring-view/animating-width
lesson_id: dynamic-island/ring-view
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: ring-view
lesson_title: Ring view
heading: Animating width
url_path: /lessons/dynamic-island/ring-view/#animating-width
markdown_path: /md/sections/dynamic-island/ring-view/animating-width.md
excerpt:
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.

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

<motion.div
  className="relative flex h-7 items-center justify-between px-2.5"
  animate={{ width: isSilent ? 148 : 128 }}
>
  ...
</motion.div>

<motion.div
  className="relative flex h-7 items-center justify-between px-2.5"
  animate={{ width: isSilent ? 148 : 128 }}
>
  ...
</motion.div>

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.

<motion.div
  layout
  style={{ borderRadius: 9999 }}
  className="h-fit min-w-[100px] overflow-hidden bg-black"
>
  {content}
</motion.div>

<motion.div
  layout
  style={{ borderRadius: 9999 }}
  className="h-fit min-w-[100px] overflow-hidden bg-black"
>
  {content}
</motion.div>


id: dynamic-island/ring-view/animating-text
lesson_id: dynamic-island/ring-view
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: ring-view
lesson_title: Ring view
heading: Animating text
url_path: /lessons/dynamic-island/ring-view/#animating-text
markdown_path: /md/sections/dynamic-island/ring-view/animating-text.md
excerpt:
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.

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

"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 <Ring />;
    case "idle":
      return <div className="h-7" />;
  }
}, [view]);

return (
  <div>
    <div className="flex h-[160px] justify-center">
      <motion.div
        layout
        style={{ borderRadius: 9999 }}
        className="h-fit min-w-[100px] overflow-hidden bg-black"
      >
        {content}
      </motion.div>
    </div>
    <div className="flex justify-center gap-4">
      <button
        type="button"
        className="rounded-full w-32 h-10 bg-white px-2.5 py-1.5 text-sm font-medium text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
        onClick={() => setView("idle")}
      >
        Idle
      </button>
      <button
        type="button"
        className="rounded-full w-32 h-10 bg-white px-2.5 py-1.5 text-sm font-medium text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
        onClick={() => setView("ring")}
      >
        Ring
      </button>
    </div>
  </div>
);
}


id: dynamic-island/ring-view/animating-the-bell
lesson_id: dynamic-island/ring-view
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: ring-view
lesson_title: Ring view
heading: Animating the bell
url_path: /lessons/dynamic-island/ring-view/#animating-the-bell
markdown_path: /md/sections/dynamic-island/ring-view/animating-the-bell.md
excerpt:
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.

body:
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:

[0, 20, -15, 12.5, -10, 10, -7.5, 7.5, -5, 5, 0]

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

<motion.div
  className="absolute inset-0"
  initial={false}
  animate={{
    rotate: silent
      ? [0, -15, 5, -2, 0]
	  : [0, 20, -15, 12.5, -10, 10, -7.5, 7.5, -5, 5, 0],
	  x: silent ? 8.5 : 0,
    }}
>
  ...
</motion.div>

<motion.div
  className="absolute inset-0"
  initial={false}
  animate={{
    rotate: silent
      ? [0, -15, 5, -2, 0]
	  : [0, 20, -15, 12.5, -10, 10, -7.5, 7.5, -5, 5, 0],
	  x: silent ? 8.5 : 0,
    }}
>
  ...
</motion.div>

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.

"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 <Ring />;
    case "idle":
      return <div className="h-7" />;
  }
}, [view]);

return (
  <div>
    <div className="flex h-[160px] justify-center">
      <motion.div
        layout
        style={{ borderRadius: 9999 }}
        className="h-fit min-w-[100px] overflow-hidden bg-black"
      >
        {content}
      </motion.div>
    </div>
    <div className="flex justify-center gap-4">
      <button
        type="button"
        className="rounded-full w-32 h-10 bg-white px-2.5 py-1.5 text-sm font-medium text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
        onClick={() => setView("idle")}
      >
        Idle
      </button>
      <button
        type="button"
        className="rounded-full w-32 h-10 bg-white px-2.5 py-1.5 text-sm font-medium text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
        onClick={() => setView("ring")}
      >
        Ring
      </button>
    </div>
  </div>
);
}


id: dynamic-island/ring-view/a-small-detail
lesson_id: dynamic-island/ring-view
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: ring-view
lesson_title: Ring view
heading: A small detail
url_path: /lessons/dynamic-island/ring-view/#a-small-detail
markdown_path: /md/sections/dynamic-island/ring-view/a-small-detail.md
excerpt:
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!

body:
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!


id: dynamic-island/the-design/studying-the-design
lesson_id: dynamic-island/the-design
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: the-design
lesson_title: The Design
heading: Studying the design
url_path: /lessons/dynamic-island/the-design/#studying-the-design
markdown_path: /md/sections/dynamic-island/the-design/studying-the-design.md
excerpt:
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.

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


id: dynamic-island/the-design/video-reference
lesson_id: dynamic-island/the-design
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: the-design
lesson_title: The Design
heading: Video reference
url_path: /lessons/dynamic-island/the-design/#video-reference
markdown_path: /md/sections/dynamic-island/the-design/video-reference.md
excerpt:
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.

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


id: dynamic-island/the-design/morphing
lesson_id: dynamic-island/the-design
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: the-design
lesson_title: The Design
heading: Morphing
url_path: /lessons/dynamic-island/the-design/#morphing
markdown_path: /md/sections/dynamic-island/the-design/morphing.md
excerpt:
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.

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


id: dynamic-island/the-design/views
lesson_id: dynamic-island/the-design
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: the-design
lesson_title: The Design
heading: Views
url_path: /lessons/dynamic-island/the-design/#views
markdown_path: /md/sections/dynamic-island/the-design/views.md
excerpt:
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.

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


id: dynamic-island/timer-view/overview
lesson_id: dynamic-island/timer-view
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: timer-view
lesson_title: Timer view
heading: Overview
url_path: /lessons/dynamic-island/timer-view/#overview
markdown_path: /md/sections/dynamic-island/timer-view/overview.md
excerpt:
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.

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

"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 <Ring />;
  case "timer":
	return <Timer />;
    case "idle":
      return <div className="h-7" />;
  }
}, [view]);

return (
  <div>
    <div className="flex h-[160px] justify-center">
      <div className="h-fit min-w-[100px] overflow-hidden rounded-full bg-black">
        {content}
      </div>
    </div>
  </div>
);
}


id: dynamic-island/timer-view/countdown-animation
lesson_id: dynamic-island/timer-view
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: timer-view
lesson_title: Timer view
heading: Countdown animation
url_path: /lessons/dynamic-island/timer-view/#countdown-animation
markdown_path: /md/sections/dynamic-island/timer-view/countdown-animation.md
excerpt:
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.

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

<motion.div
  className="inline-block tabular-nums"
  key={n + i}
  initial={{ y: "12px", filter: "blur(2px)", opacity: 0 }}
  animate={{ y: "0", filter: "blur(0px)", opacity: 1 }}
  exit={{ y: "-12px", filter: "blur(2px)", opacity: 0 }}
  transition={{ type: "spring", bounce: 0.35 }}
>
  {n}
</motion.div>

<motion.div
  className="inline-block tabular-nums"
  key={n + i}
  initial={{ y: "12px", filter: "blur(2px)", opacity: 0 }}
  animate={{ y: "0", filter: "blur(0px)", opacity: 1 }}
  exit={{ y: "-12px", filter: "blur(2px)", opacity: 0 }}
  transition={{ type: "spring", bounce: 0.35 }}
>
  {n}
</motion.div>

The combination of AnimatePresence and our animation gives us the following result:


id: dynamic-island/timer-view/play-button-animation
lesson_id: dynamic-island/timer-view
module_slug: dynamic-island
module_title: Dynamic Island
lesson_slug: timer-view
lesson_title: Timer view
heading: Play button animation
url_path: /lessons/dynamic-island/timer-view/#play-button-animation
markdown_path: /md/sections/dynamic-island/timer-view/play-button-animation.md
excerpt:
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?

body:
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:

<motion.svg
  key="play"
  initial={{ opacity: 0, scale: 0.5, filter: "blur(4px)" }}
  animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
  exit={{ opacity: 0, scale: 0.5, filter: "blur(4px)" }}
  transition={{ duration: 0.1 }}
  viewBox="0 0 12 14"
>
  ...
</motion.svg>

<motion.svg
  key="play"
  initial={{ opacity: 0, scale: 0.5, filter: "blur(4px)" }}
  animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
  exit={{ opacity: 0, scale: 0.5, filter: "blur(4px)" }}
  transition={{ duration: 0.1 }}
  viewBox="0 0 12 14"
>
  ...
</motion.svg>

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.

"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 <Ring />;
  case "timer":
	return <Timer />;
    case "idle":
      return <div className="h-7" />;
  }
}, [view]);

return (
  <div>
    <div className="flex h-[160px] justify-center">
      <div className="h-fit min-w-[100px] overflow-hidden rounded-full bg-black">
        {content}
      </div>
    </div>
  </div>
);
}


id: family-drawer/crossfade/overview
lesson_id: family-drawer/crossfade
module_slug: family-drawer
module_title: Family Drawer
lesson_slug: crossfade
lesson_title: Crossfade
heading: Overview
url_path: /lessons/family-drawer/crossfade/#overview
markdown_path: /md/sections/family-drawer/crossfade/overview.md
excerpt:
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.

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

const content = useMemo(() => {
  switch (view) {
    case "default":
      return <DefaultView setView={setView} />;
    case "remove":
      return <RemoveWallet setView={setView} />;
    case "phrase":
      return <Phrase setView={setView} />;
    case "key":
      return <Key setView={setView} />;
  }
}, [view]);

const content = useMemo(() => {
  switch (view) {
    case "default":
      return <DefaultView setView={setView} />;
    case "remove":
      return <RemoveWallet setView={setView} />;
    case "phrase":
      return <Phrase setView={setView} />;
    case "key":
      return <Key setView={setView} />;
  }
}, [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.

<button
  className={clsx(
    "grid place-items-center",
    className,
  )}
/>

<button
  className={clsx(
    "grid place-items-center",
    className,
  )}
/>

Or when I need to add a class conditionally when the state of the component changes for example.

<div
  className={clsx("size-16", {
    "bg-blue-400": isActive,
    "bg-gray-400": isDisabled,
  })}
/>

<div
  className={clsx("size-16", {
    "bg-blue-400": isActive,
    "bg-gray-400": isDisabled,
  })}
/>

The buttons now have a small scale transition on press, we go from 1 to 0.95 with the default transition settings from tailwind.

Last thing is the font. Family uses a custom font in their app, it’s a sans-serif font that is a bit rounded. The closest, free font I could find was Open Runde. It’s a rounded variant of Inter. This is important as in recreations like this one it’s not only about the animations, it has to look like the original as well.

In the code editor here I assign the font inline, that’s a limitation of Sandpack, which I use under the hood. I don’t usually inline this type of styles.


id: family-drawer/crossfade/crossfade
lesson_id: family-drawer/crossfade
module_slug: family-drawer
module_title: Family Drawer
lesson_slug: crossfade
lesson_title: Crossfade
heading: Crossfade
url_path: /lessons/family-drawer/crossfade/#crossfade
markdown_path: /md/sections/family-drawer/crossfade/crossfade.md
excerpt:
By updating the styles we get a lot closer to the actual Family iOS drawer. Let’s now add the crossfade animation to it. This is how the end result should look like::

body:
By updating the styles we get a lot closer to the actual Family iOS drawer. Let’s now add the crossfade animation to it. This is how the end result should look like::

"use client";

import { useMemo, useState } from "react";
import { Drawer } from "vaul";
import useMeasure from "react-use-measure";
import { motion, AnimatePresence } from "motion/react";
import { DefaultView, Key, Phrase, RemoveWallet } from "./components";
import { CloseIcon } from "./icons";

export default function FamilyDrawer() {
const [isOpen, setIsOpen] = useState(false);
const [view, setView] = useState("default");
const [elementRef, bounds] = useMeasure();

const content = useMemo(() => {
  switch (view) {
    case "default":
      return <DefaultView setView={setView} />;
    case "remove":
      return <RemoveWallet setView={setView} />;
    case "phrase":
      return <Phrase setView={setView} />;
    case "key":
      return <Key setView={setView} />;
  }
}, [view]);

return (
  <>
    <button
      className="fixed top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 h-[44px] rounded-full border border-gray-200 bg-white px-4 py-2 font-medium text-black transition-colors hover:bg-[#F9F9F8] focus-visible:shadow-focus-ring-button md:font-medium"
      onClick={() => setIsOpen(true)}
	style={{ fontFamily: "Open Runde" }}
    >
      Try it out
    </button>
    <Drawer.Root open={isOpen} onOpenChange={setIsOpen}>
      <Drawer.Portal>
        <Drawer.Overlay
          className="fixed inset-0 z-10 bg-black/30"
          onClick={() => setIsOpen(false)}
        />
        <Drawer.Content
          asChild
          className="fixed inset-x-4 bottom-4 z-10 mx-auto max-w-[361px] overflow-hidden rounded-[36px] bg-[#FEFFFE] outline-hidden md:mx-auto md:w-full"
        >
          <motion.div animate={{ height: bounds.height }}>
            <Drawer.Close asChild>
              <button
                data-vaul-no-drag=""
                className="absolute right-8 top-7 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-[#F7F8F9] text-[#949595] transition-transform focus:scale-95 focus-visible:shadow-focus-ring-button active:scale-75"
              >
                <CloseIcon />
              </button>
            </Drawer.Close>
            <div ref={elementRef} className="px-6 pb-6 pt-2.5 antialiased" style={{ fontFamily: "Open Runde" }}>
              {content}
            </div>
          </motion.div>
        </Drawer.Content>
      </Drawer.Portal>
    </Drawer.Root>
  </>
);
}


id: family-drawer/first-animations/using-existing-solutions
lesson_id: family-drawer/first-animations
module_slug: family-drawer
module_title: Family Drawer
lesson_slug: first-animations
lesson_title: First animations
heading: Using existing solutions
url_path: /lessons/family-drawer/first-animations/#using-existing-solutions
markdown_path: /md/sections/family-drawer/first-animations/using-existing-solutions.md
excerpt:
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.

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


id: family-drawer/first-animations/tip
lesson_id: family-drawer/first-animations
module_slug: family-drawer
module_title: Family Drawer
lesson_slug: first-animations
lesson_title: First animations
heading: Tip
url_path: /lessons/family-drawer/first-animations/#tip
markdown_path: /md/sections/family-drawer/first-animations/tip.md
excerpt:
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.

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

"use client";

import { useState } from "react";
import { Drawer } from 'vaul';

export default function FamilyDrawer() {
const [isOpen, setIsOpen] = useState(false);

return (
  <>
    <button
      className="h-[44px] fixed top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 px-6 rounded-full bg-white py-2 font-medium text-black border border-gray-200 transition-colors hover:bg-[#F9F9F8] focus-visible:shadow-focus-ring-button md:font-medium"
      onClick={() => setIsOpen(true)}
    >
      Try it out
    </button>
    {isOpen ? (
      <>
        <div
          className="fixed inset-0 z-10 bg-black/30"
          onClick={() => setIsOpen(false)}
        />
        <div className="fixed inset-x-4 bottom-4 z-10 mx-auto h-64 max-w-[361px] overflow-hidden rounded-[36px] bg-[#FEFFFE] outline-hidden md:mx-auto md:w-full"></div>
      </>
    ) : 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.


id: family-drawer/first-animations/height-animation
lesson_id: family-drawer/first-animations
module_slug: family-drawer
module_title: Family Drawer
lesson_slug: first-animations
lesson_title: First animations
heading: Height animation
url_path: /lessons/family-drawer/first-animations/#height-animation
markdown_path: /md/sections/family-drawer/first-animations/height-animation.md
excerpt:
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.

body:
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!

"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 (
        <div>
          <p>This is the default case</p>
          <div className="mt-6 flex flex-col items-start gap-2">
            <button onClick={() => setView("key")}>Key</button>
            <button onClick={() => setView("phrase")}>Phrase</button>
            <button onClick={() => setView("remove")}>Remove</button>
          </div>
        </div>
      );
    case "remove":
      return (
        <div>
          <p>
            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.
          </p>
          <button onClick={() => setView("default")}>Go back</button>
        </div>
      );

    case "phrase":
      return (
        <div>
          <p>
            Keep your Secret Phrase safe. Don’t share it with anyone else. If
            you lose it, we can’t recover it.
          </p>
          <button onClick={() => setView("default")}>Go back</button>
        </div>
      );
    case "key":
      return (
        <div>
          <p>
            Your Private Key is the key used to back up your wallet. Keep it
            secret and secure at all times.
          </p>
          <button onClick={() => setView("default")}>Go back</button>
        </div>
      );
  }
}, [view]);

return (
  <>
    <button
      className="fixed top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 h-[44px] rounded-full border border-gray-200 bg-white px-4 py-2 font-medium text-black transition-colors hover:bg-[#F9F9F8] focus-visible:shadow-focus-ring-button md:font-medium"
      onClick={() => setIsOpen(true)}
    >
      Try it out
    </button>
    <Drawer.Root open={isOpen} onOpenChange={setIsOpen}>
      <Drawer.Portal>
        <Drawer.Overlay
          className="fixed inset-0 z-10 bg-black/30"
          onClick={() => setIsOpen(false)}
        />
        <Drawer.Content
          className="fixed inset-x-4 bottom-4 z-10 mx-auto max-w-[361px] overflow-hidden rounded-[36px] bg-[#FEFFFE] outline-hidden md:mx-auto md:w-full p-6"
        >
          {content}
        </Drawer.Content>
      </Drawer.Portal>
    </Drawer.Root>
  </>
);
}

In the next part, we’ll focus on the cross fade animation.


id: family-drawer/the-analysis/recordings
lesson_id: family-drawer/the-analysis
module_slug: family-drawer
module_title: Family Drawer
lesson_slug: the-analysis
lesson_title: The analysis
heading: Recordings
url_path: /lessons/family-drawer/the-analysis/#recordings
markdown_path: /md/sections/family-drawer/the-analysis/recordings.md
excerpt:
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.

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


id: family-drawer/the-analysis/summary
lesson_id: family-drawer/the-analysis
module_slug: family-drawer
module_title: Family Drawer
lesson_slug: the-analysis
lesson_title: The analysis
heading: Summary
url_path: /lessons/family-drawer/the-analysis/#summary
markdown_path: /md/sections/family-drawer/the-analysis/summary.md
excerpt:
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:

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


id: family-drawer/the-analysis/smoothness
lesson_id: family-drawer/the-analysis
module_slug: family-drawer
module_title: Family Drawer
lesson_slug: the-analysis
lesson_title: The analysis
heading: Smoothness
url_path: /lessons/family-drawer/the-analysis/#smoothness
markdown_path: /md/sections/family-drawer/the-analysis/smoothness.md
excerpt:
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.

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


id: family-drawer/the-finishing-touch/overview
lesson_id: family-drawer/the-finishing-touch
module_slug: family-drawer
module_title: Family Drawer
lesson_slug: the-finishing-touch
lesson_title: The finishing touch
heading: Overview
url_path: /lessons/family-drawer/the-finishing-touch/#overview
markdown_path: /md/sections/family-drawer/the-finishing-touch/overview.md
excerpt:
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.

body:
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:


id: family-drawer/the-finishing-touch/dynamic-opacity-transition
lesson_id: family-drawer/the-finishing-touch
module_slug: family-drawer
module_title: Family Drawer
lesson_slug: the-finishing-touch
lesson_title: The finishing touch
heading: Dynamic opacity transition
url_path: /lessons/family-drawer/the-finishing-touch/#dynamic-opacity-transition
markdown_path: /md/sections/family-drawer/the-finishing-touch/dynamic-opacity-transition.md
excerpt:
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.

body:
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!

"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 <DefaultView setView={setView} />;
    case "remove":
      return <RemoveWallet setView={setView} />;
    case "phrase":
      return <Phrase setView={setView} />;
    case "key":
      return <Key setView={setView} />;
  }
}, [view]);

return (
  <>
    <button
      className="fixed top-1/2 left-1/2 antialiased -translate-y-1/2 -translate-x-1/2 h-[44px] rounded-full border border-gray-200 bg-white px-4 py-2 font-medium text-black transition-colors hover:bg-[#F9F9F8] focus-visible:shadow-focus-ring-button md:font-medium"
      onClick={() => setIsOpen(true)}
	style={{ fontFamily: "Open Runde" }}
    >
      Try it out
    </button>
    <Drawer.Root open={isOpen} onOpenChange={setIsOpen}>
      <Drawer.Portal>
        <Drawer.Overlay
          className="fixed inset-0 z-10 bg-black/30"
          onClick={() => setIsOpen(false)}
        />
        <Drawer.Content
          asChild
          className="fixed inset-x-4 bottom-4 z-10 mx-auto max-w-[361px] overflow-hidden rounded-[36px] bg-[#FEFFFE] outline-hidden md:mx-auto md:w-full"
		style={{ fontFamily: "Open Runde" }}
        >
          <motion.div
            animate={{
              height: bounds.height,
              transition: {
                duration: 0.27,
                ease: [0.25, 1, 0.5, 1],
              },
            }}
          >
            <Drawer.Close asChild>
              <button
                data-vaul-no-drag=""
                className="absolute right-8 top-7 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-[#F7F8F9] text-[#949595] transition-transform focus:scale-95 focus-visible:shadow-focus-ring-button active:scale-75"
              >
                <CloseIcon />
              </button>
            </Drawer.Close>
            <div ref={elementRef} className="px-6 pb-6 pt-2.5 antialiased">
              <AnimatePresence initial={false} mode="popLayout" custom={view}>
                <motion.div
                  initial={{ opacity: 0, scale: 0.96 }}
                  animate={{ opacity: 1, scale: 1, y: 0 }}
                  exit={{ opacity: 0, scale: 0.96 }}
                  key={view}
                  transition={{
                    duration: 0.27,
                    ease: [0.26, 0.08, 0.25, 1],
                  }}
                >
                  {content}
                </motion.div>
              </AnimatePresence>
            </div>
          </motion.div>
        </Drawer.Content>
      </Drawer.Portal>
    </Drawer.Root>
  </>
);
}


id: family-drawer/the-finishing-touch/vaul-s-duration
lesson_id: family-drawer/the-finishing-touch
module_slug: family-drawer
module_title: Family Drawer
lesson_slug: the-finishing-touch
lesson_title: The finishing touch
heading: Vaul’s duration
url_path: /lessons/family-drawer/the-finishing-touch/#vaul-s-duration
markdown_path: /md/sections/family-drawer/the-finishing-touch/vaul-s-duration.md
excerpt:
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.

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

[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);
}

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


id: family-drawer/the-finishing-touch/conclusion
lesson_id: family-drawer/the-finishing-touch
module_slug: family-drawer
module_title: Family Drawer
lesson_slug: the-finishing-touch
lesson_title: The finishing touch
heading: Conclusion
url_path: /lessons/family-drawer/the-finishing-touch/#conclusion
markdown_path: /md/sections/family-drawer/the-finishing-touch/conclusion.md
excerpt:
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.

body:
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!

"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 <DefaultView setView={setView} />;
    case "remove":
      return <RemoveWallet setView={setView} />;
    case "phrase":
      return <Phrase setView={setView} />;
    case "key":
      return <Key setView={setView} />;
  }
}, [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 (
  <>
    <button
      className="fixed top-1/2 left-1/2 antialiased -translate-y-1/2 -translate-x-1/2 h-[44px] rounded-full border border-gray-200 bg-white px-4 py-2 font-medium text-black transition-colors hover:bg-[#F9F9F8] focus-visible:shadow-focus-ring-button md:font-medium"
      onClick={() => setIsOpen(true)}
	style={{ fontFamily: "Open Runde" }}
    >
      Try it out
    </button>
    <Drawer.Root open={isOpen} onOpenChange={setIsOpen}>
      <Drawer.Portal>
        <Drawer.Overlay
          className="fixed inset-0 z-10 bg-black/30"
          onClick={() => setIsOpen(false)}
        />
        <Drawer.Content
          asChild
          className="fixed inset-x-4 bottom-4 z-10 mx-auto max-w-[361px] overflow-hidden rounded-[36px] bg-[#FEFFFE] outline-hidden md:mx-auto md:w-full"
		style={{ fontFamily: "Open Runde" }}
        >
          <motion.div
            animate={{
              height: bounds.height,
              transition: {
                duration: 0.27,
                ease: [0.25, 1, 0.5, 1],
              },
            }}
          >
            <Drawer.Close asChild>
              <button
                data-vaul-no-drag=""
                className="absolute right-8 top-7 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-[#F7F8F9] text-[#949595] transition-transform focus:scale-95 focus-visible:shadow-focus-ring-button active:scale-75"
              >
                <CloseIcon />
              </button>
            </Drawer.Close>
            <div ref={elementRef} className="px-6 pb-6 pt-2.5 antialiased">
              <AnimatePresence initial={false} mode="popLayout" custom={view}>
                <motion.div
                  initial={{ opacity: 0, scale: 0.96 }}
                  animate={{ opacity: 1, scale: 1, y: 0 }}
                  exit={{ opacity: 0, scale: 0.96 }}
                  key={view}
                  transition={{
                    duration: opacityDuration,
                    ease: [0.26, 0.08, 0.25, 1],
                  }}
                >
                  {content}
                </motion.div>
              </AnimatePresence>
            </div>
          </motion.div>
        </Drawer.Content>
      </Drawer.Portal>
    </Drawer.Root>
  </>
);
}


id: framer-motion/animating-in-public/overview
lesson_id: framer-motion/animating-in-public
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: animating-in-public
lesson_title: Animating in public
heading: Overview
excerpt: I got my first job at an American start up through Twitter, because I shared my work there.
url_path: /lessons/framer-motion/animating-in-public/#overview
markdown_path: /md/sections/framer-motion/animating-in-public/overview.md
body:
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.


id: framer-motion/animating-in-public/what-does-this-have-to-do-with-animations
lesson_id: framer-motion/animating-in-public
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: animating-in-public
lesson_title: Animating in public
heading: What does this have to do with animations?
url_path: /lessons/framer-motion/animating-in-public/#what-does-this-have-to-do-with-animations
markdown_path: /md/sections/framer-motion/animating-in-public/what-does-this-have-to-do-with-animations.md
excerpt:
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.

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


id: framer-motion/animating-in-public/how-to-record-animations
lesson_id: framer-motion/animating-in-public
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: animating-in-public
lesson_title: Animating in public
heading: How to record animations
excerpt: Here are a few tips:
url_path: /lessons/framer-motion/animating-in-public/#how-to-record-animations
markdown_path: /md/sections/framer-motion/animating-in-public/how-to-record-animations.md
body:
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.


id: framer-motion/feedback-popover/button-to-popover
lesson_id: framer-motion/feedback-popover
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: feedback-popover
lesson_title: Feedback popover
heading: Button to popover
url_path: /lessons/framer-motion/feedback-popover/#button-to-popover
markdown_path: /md/sections/framer-motion/feedback-popover/button-to-popover.md
excerpt:
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.

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

"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 (
  <div className="feedback-wrapper">
    <button
      onClick={() => {
        setOpen(true);
        setFormState("idle");
        setFeedback("");
      }}
      className="feedback-button"
      style={{ borderRadius: 8 }}
    >
      <span>Feedback</span>
    </button>
    {open ? (
      <div
        className="feedback-popover"
        style={{ borderRadius: 12 }}
	  ref={ref}
      >
        <span
          aria-hidden
          className="placeholder"
          data-feedback={feedback ? "true" : "false"}
        >
          Feedback
        </span>

        {formState === "success" ? (
          <div className="success-wrapper">
            <svg
              width="32"
              height="32"
              viewBox="0 0 32 32"
              fill="none"
              xmlns="http://www.w3.org/2000/svg"
            >
              <path
                d="M27.6 16C27.6 17.5234 27.3 19.0318 26.717 20.4392C26.1341 21.8465 25.2796 23.1253 24.2025 24.2025C23.1253 25.2796 21.8465 26.1341 20.4392 26.717C19.0318 27.3 17.5234 27.6 16 27.6C14.4767 27.6 12.9683 27.3 11.5609 26.717C10.1535 26.1341 8.87475 25.2796 7.79759 24.2025C6.72043 23.1253 5.86598 21.8465 5.28302 20.4392C4.70007 19.0318 4.40002 17.5234 4.40002 16C4.40002 12.9235 5.62216 9.97301 7.79759 7.79759C9.97301 5.62216 12.9235 4.40002 16 4.40002C19.0765 4.40002 22.027 5.62216 24.2025 7.79759C26.3779 9.97301 27.6 12.9235 27.6 16Z"
                fill="#2090FF"
                fillOpacity="0.16"
              />
              <path
                d="M12.1334 16.9667L15.0334 19.8667L19.8667 13.1M27.6 16C27.6 17.5234 27.3 19.0318 26.717 20.4392C26.1341 21.8465 25.2796 23.1253 24.2025 24.2025C23.1253 25.2796 21.8465 26.1341 20.4392 26.717C19.0318 27.3 17.5234 27.6 16 27.6C14.4767 27.6 12.9683 27.3 11.5609 26.717C10.1535 26.1341 8.87475 25.2796 7.79759 24.2025C6.72043 23.1253 5.86598 21.8465 5.28302 20.4392C4.70007 19.0318 4.40002 17.5234 4.40002 16C4.40002 12.9235 5.62216 9.97301 7.79759 7.79759C9.97301 5.62216 12.9235 4.40002 16 4.40002C19.0765 4.40002 22.027 5.62216 24.2025 7.79759C26.3779 9.97301 27.6 12.9235 27.6 16Z"
                stroke="#2090FF"
                strokeWidth="2.4"
                strokeLinecap="round"
                strokeLinejoin="round"
              />
            </svg>
            <h3>Feedback received!</h3>
            <p>Thanks for helping me improve Sonner.</p>
          </div>
        ) : (
          <form
            onSubmit={(e) => {
			e.preventDefault();
			if(!feedback) return;
              submit();
            }}
            className="feedback-form"
          >
            <textarea
              autoFocus
			placeholder="Feedback"
              onChange={(e) => setFeedback(e.target.value)}
              className="textarea"
			required
            />
            <div className="feedback-footer">
              <svg
                className="dotted-line"
                width="352"
                height="2"
                viewBox="0 0 352 2"
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
              >
                <path d="M0 1H352" stroke="#E6E7E8" strokeDasharray="4 4" />
              </svg>
              <div className="half-circle-left">
                <svg
                  width="6"
                  height="12"
                  viewBox="0 0 6 12"
                  fill="none"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <g clipPath="url(#clip0_2029_22)">
                    <path
                      d="M0 2C0.656613 2 1.30679 2.10346 1.91341 2.30448C2.52005 2.5055 3.07124 2.80014 3.53554 3.17157C3.99982 3.54301 4.36812 3.98396 4.6194 4.46927C4.87067 4.95457 5 5.47471 5 6C5 6.52529 4.87067 7.04543 4.6194 7.53073C4.36812 8.01604 3.99982 8.45699 3.53554 8.82843C3.07124 9.19986 2.52005 9.4945 1.91341 9.69552C1.30679 9.89654 0.656613 10 0 10V6V2Z"
                      fill="#F5F6F7"
                    />
                    <path
                      d="M1 12V10C2.06087 10 3.07828 9.57857 3.82843 8.82843C4.57857 8.07828 5 7.06087 5 6C5 4.93913 4.57857 3.92172 3.82843 3.17157C3.07828 2.42143 2.06087 2 1 2V0"
                      stroke="#E6E7E8"
                      strokeWidth="1"
                      strokeLinejoin="round"
                    />
                  </g>
                  <defs>
                    <clipPath id="clip0_2029_22">
                      <rect width="6" height="12" fill="white" />
                    </clipPath>
                  </defs>
                </svg>
              </div>

              <div className="half-circle-right">
                <svg
                  width="6"
                  height="12"
                  viewBox="0 0 6 12"
                  fill="none"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <g clipPath="url(#clip0_2029_22)">
                    <path
                      d="M0 2C0.656613 2 1.30679 2.10346 1.91341 2.30448C2.52005 2.5055 3.07124 2.80014 3.53554 3.17157C3.99982 3.54301 4.36812 3.98396 4.6194 4.46927C4.87067 4.95457 5 5.47471 5 6C5 6.52529 4.87067 7.04543 4.6194 7.53073C4.36812 8.01604 3.99982 8.45699 3.53554 8.82843C3.07124 9.19986 2.52005 9.4945 1.91341 9.69552C1.30679 9.89654 0.656613 10 0 10V6V2Z"
                      fill="#F5F6F7"
                    />
                    <path
                      d="M1 12V10C2.06087 10 3.07828 9.57857 3.82843 8.82843C4.57857 8.07828 5 7.06087 5 6C5 4.93913 4.57857 3.92172 3.82843 3.17157C3.07828 2.42143 2.06087 2 1 2V0"
                      stroke="#E6E7E8"
                      strokeWidth="1"
                      strokeLinejoin="round"
                    />
                  </g>
                  <defs>
                    <clipPath id="clip0_2029_22">
                      <rect width="6" height="12" fill="white" />
                    </clipPath>
                  </defs>
                </svg>
              </div>

              <button type="submit" className="submit-button">
                {/* We already built this one in the animate presence part */}
                <AnimatePresence mode="popLayout" initial={false}>
                  <motion.span
                    transition={{
                      type: "spring",
                      duration: 0.3,
                      bounce: 0,
                    }}
                    initial={{ opacity: 0, y: -25 }}
                    animate={{ opacity: 1, y: 0 }}
                    exit={{ opacity: 0, y: 25 }}
                    key={formState}
                  >
                    {formState === "loading" ? (
                      <Spinner size={14} color="rgba(255, 255, 255, 0.65)" />
                    ) : (
                      <span>Send feedback</span>
                    )}
                  </motion.span>
                </AnimatePresence>
              </button>
            </div>
          </form>
        )}
      </div>
    ) : null}
  </div>
);
}


id: framer-motion/feedback-popover/success-state
lesson_id: framer-motion/feedback-popover
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: feedback-popover
lesson_title: Feedback popover
heading: Success state
url_path: /lessons/framer-motion/feedback-popover/#success-state
markdown_path: /md/sections/framer-motion/feedback-popover/success-state.md
excerpt:
Let’s now animate the success state, the form should disappear to the bottom, and the success ui should come from top, with a slight blur.

body:
Let’s now animate the success state, the form should disappear to the bottom, and the success ui should come from top, with a slight blur.

Success criteria:

The form and the success ui should animate simultaneously. The success state should come from the top. Exit animation should morph the success state into the button.

"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 (
  <div className="feedback-wrapper">
    <motion.button
      layoutId="wrapper"
      onClick={() => {
        setOpen(true);
        setFormState("idle");
        setFeedback("");
      }}
      key="button"
      className="feedback-button"
      style={{ borderRadius: 8 }}
    >
      <motion.span layoutId="title">Feedback</motion.span>
    </motion.button>
    <AnimatePresence>
      {open ? (
        <motion.div
	    ref={ref}
          layoutId="wrapper"
          className="feedback-popover"
          style={{ borderRadius: 12 }}
        >
          <motion.span
            aria-hidden
            className="placeholder"
            layoutId="title"
            data-success={formState === "success" ? "true" : "false"}
            data-feedback={feedback ? "true" : "false"}
          >
            Feedback
          </motion.span>

          {formState === "success" ? (
            <div className="success-wrapper" key="success">
              <svg
                width="32"
                height="32"
                viewBox="0 0 32 32"
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
              >
                <path
                  d="M27.6 16C27.6 17.5234 27.3 19.0318 26.717 20.4392C26.1341 21.8465 25.2796 23.1253 24.2025 24.2025C23.1253 25.2796 21.8465 26.1341 20.4392 26.717C19.0318 27.3 17.5234 27.6 16 27.6C14.4767 27.6 12.9683 27.3 11.5609 26.717C10.1535 26.1341 8.87475 25.2796 7.79759 24.2025C6.72043 23.1253 5.86598 21.8465 5.28302 20.4392C4.70007 19.0318 4.40002 17.5234 4.40002 16C4.40002 12.9235 5.62216 9.97301 7.79759 7.79759C9.97301 5.62216 12.9235 4.40002 16 4.40002C19.0765 4.40002 22.027 5.62216 24.2025 7.79759C26.3779 9.97301 27.6 12.9235 27.6 16Z"
                  fill="#2090FF"
                  fillOpacity="0.16"
                />
                <path
                  d="M12.1334 16.9667L15.0334 19.8667L19.8667 13.1M27.6 16C27.6 17.5234 27.3 19.0318 26.717 20.4392C26.1341 21.8465 25.2796 23.1253 24.2025 24.2025C23.1253 25.2796 21.8465 26.1341 20.4392 26.717C19.0318 27.3 17.5234 27.6 16 27.6C14.4767 27.6 12.9683 27.3 11.5609 26.717C10.1535 26.1341 8.87475 25.2796 7.79759 24.2025C6.72043 23.1253 5.86598 21.8465 5.28302 20.4392C4.70007 19.0318 4.40002 17.5234 4.40002 16C4.40002 12.9235 5.62216 9.97301 7.79759 7.79759C9.97301 5.62216 12.9235 4.40002 16 4.40002C19.0765 4.40002 22.027 5.62216 24.2025 7.79759C26.3779 9.97301 27.6 12.9235 27.6 16Z"
                  stroke="#2090FF"
                  strokeWidth="2.4"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                />
              </svg>
              <h3>Feedback received!</h3>
              <p>Thanks for helping me improve Sonner.</p>
            </div>
          ) : (
            <form
		    key="form"
              onSubmit={(e) => {
		e.preventDefault();
			  if(!feedback) return;
                submit();
              }}
              className="feedback-form"
            >
              <textarea
                autoFocus
			  placeholder="Feedback"
                onChange={(e) => setFeedback(e.target.value)}
                className="textarea"
			  required
              />
              <div className="feedback-footer">
                <svg
                  className="dotted-line"
                  width="352"
                  height="2"
                  viewBox="0 0 352 2"
                  fill="none"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <path d="M0 1H352" stroke="#E6E7E8" strokeDasharray="4 4" />
                </svg>
                <div className="half-circle-left">
                  <svg
                    width="6"
                    height="12"
                    viewBox="0 0 6 12"
                    fill="none"
                    xmlns="http://www.w3.org/2000/svg"
                  >
                    <g clipPath="url(#clip0_2029_22)">
                      <path
                        d="M0 2C0.656613 2 1.30679 2.10346 1.91341 2.30448C2.52005 2.5055 3.07124 2.80014 3.53554 3.17157C3.99982 3.54301 4.36812 3.98396 4.6194 4.46927C4.87067 4.95457 5 5.47471 5 6C5 6.52529 4.87067 7.04543 4.6194 7.53073C4.36812 8.01604 3.99982 8.45699 3.53554 8.82843C3.07124 9.19986 2.52005 9.4945 1.91341 9.69552C1.30679 9.89654 0.656613 10 0 10V6V2Z"
                        fill="#F5F6F7"
                      />
                      <path
                        d="M1 12V10C2.06087 10 3.07828 9.57857 3.82843 8.82843C4.57857 8.07828 5 7.06087 5 6C5 4.93913 4.57857 3.92172 3.82843 3.17157C3.07828 2.42143 2.06087 2 1 2V0"
                        stroke="#E6E7E8"
                        strokeWidth="1"
                        strokeLinejoin="round"
                      />
                    </g>
                    <defs>
                      <clipPath id="clip0_2029_22">
                        <rect width="6" height="12" fill="white" />
                      </clipPath>
                    </defs>
                  </svg>
                </div>

                <div className="half-circle-right">
                  <svg
                    width="6"
                    height="12"
                    viewBox="0 0 6 12"
                    fill="none"
                    xmlns="http://www.w3.org/2000/svg"
                  >
                    <g clipPath="url(#clip0_2029_22)">
                      <path
                        d="M0 2C0.656613 2 1.30679 2.10346 1.91341 2.30448C2.52005 2.5055 3.07124 2.80014 3.53554 3.17157C3.99982 3.54301 4.36812 3.98396 4.6194 4.46927C4.87067 4.95457 5 5.47471 5 6C5 6.52529 4.87067 7.04543 4.6194 7.53073C4.36812 8.01604 3.99982 8.45699 3.53554 8.82843C3.07124 9.19986 2.52005 9.4945 1.91341 9.69552C1.30679 9.89654 0.656613 10 0 10V6V2Z"
                        fill="#F5F6F7"
                      />
                      <path
                        d="M1 12V10C2.06087 10 3.07828 9.57857 3.82843 8.82843C4.57857 8.07828 5 7.06087 5 6C5 4.93913 4.57857 3.92172 3.82843 3.17157C3.07828 2.42143 2.06087 2 1 2V0"
                        stroke="#E6E7E8"
                        strokeWidth="1"
                        strokeLinejoin="round"
                      />
                    </g>
                    <defs>
                      <clipPath id="clip0_2029_22">
                        <rect width="6" height="12" fill="white" />
                      </clipPath>
                    </defs>
                  </svg>
                </div>

                <button type="submit" className="submit-button">
                  {/* We already built this one in the animate presence part */}
                  <AnimatePresence mode="popLayout" initial={false}>
                    <motion.span
                      transition={{
                        type: "spring",
                        duration: 0.3,
                        bounce: 0,
                      }}
                      initial={{ opacity: 0, y: -25 }}
                      animate={{ opacity: 1, y: 0 }}
                      exit={{ opacity: 0, y: 25 }}
                      key={formState}
                    >
                      {formState === "loading" ? (
                        <Spinner
                          size={14}
                          color="rgba(255, 255, 255, 0.65)"
                        />
                      ) : (
                        <span>Send feedback</span>
                      )}
                    </motion.span>
                  </AnimatePresence>
                </button>
              </div>
            </form>
          )}
        </motion.div>
      ) : null}
    </AnimatePresence>
  </div>
);
}


id: framer-motion/feedback-popover/accessibility
lesson_id: framer-motion/feedback-popover
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: feedback-popover
lesson_title: Feedback popover
heading: Accessibility
url_path: /lessons/framer-motion/feedback-popover/#accessibility
markdown_path: /md/sections/framer-motion/feedback-popover/accessibility.md
excerpt:
Even if we replace our placeholder with a span we should still provide a placeholder for the textarea, this is because the placeholder attribute is used by screen readers to provide context to the user. We can hide it visually by setting the opacity to 0.

body:
Even if we replace our placeholder with a span we should still provide a placeholder for the textarea, this is because the placeholder attribute is used by screen readers to provide context to the user. We can hide it visually by setting the opacity to 0.


id: framer-motion/feedback-popover/key-takeaways
lesson_id: framer-motion/feedback-popover
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: feedback-popover
lesson_title: Feedback popover
heading: Key takeaways
url_path: /lessons/framer-motion/feedback-popover/#key-takeaways
markdown_path: /md/sections/framer-motion/feedback-popover/key-takeaways.md
excerpt:
layoutId is very powerful, and it’s even more powerful once you become a bit creative with it. In this case, we created an illusion. The placeholder is not an actual placeholder, but it looks like one.

body:
layoutId is very powerful, and it’s even more powerful once you become a bit creative with it. In this case, we created an illusion. The placeholder is not an actual placeholder, but it looks like one.

Another good takeaway here is that the popLayout mode is often times the right mode for your animations. If you see your exit animation breaking, think about the mode prop.


id: framer-motion/hooks-and-animations/overview
lesson_id: framer-motion/hooks-and-animations
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: hooks-and-animations
lesson_title: Hooks and animations
heading: Overview
url_path: /lessons/framer-motion/hooks-and-animations/#overview
markdown_path: /md/sections/framer-motion/hooks-and-animations/overview.md
excerpt:
We’ve now animated things mainly using initial, animate and exit props. While these props are very powerful, and I personally use Framer Motion this way ~90% of the time, this library has more to offer.

body:
We’ve now animated things mainly using initial, animate and exit props. While these props are very powerful, and I personally use Framer Motion this way ~90% of the time, this library has more to offer.

Hooks like useSpring or useTransform can help us in cases where the animate prop is not enough. But before we dive into these hooks, we need to understand what motion values are.


id: framer-motion/hooks-and-animations/motion-values
lesson_id: framer-motion/hooks-and-animations
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: hooks-and-animations
lesson_title: Hooks and animations
heading: Motion values
url_path: /lessons/framer-motion/hooks-and-animations/#motion-values
markdown_path: /md/sections/framer-motion/hooks-and-animations/motion-values.md
excerpt:
Motion values are primitives from Framer Motion that update their value outside of React’s render cycle. That’s what allows us to animate at 60 frames per second as we are not triggering any re-renders each time the transform value changes for example.

body:
Motion values are primitives from Framer Motion that update their value outside of React’s render cycle. That’s what allows us to animate at 60 frames per second as we are not triggering any re-renders each time the transform value changes for example.

In the recording below the transform property changes its value multiple times per second, but because it’s not caused by a change in the state, but rather a change in the motion value, the component won’t re-render.

To create a motion value we can use the useMotionValue hook. The string or number passed to the hook will act as its initial state. motion components can consume a motion value through the style property. The translateX value of the div below will be 100px for example.

const x = useMotionValue(100);

<motion.div style={{ x }} />

const x = useMotionValue(100);

<motion.div style={{ x }} />

We can update our motion values with the set method and read their value with the get method. Notice how in the demo below when we click on the button, the motion value updates, but the rendered value is not changed. That’s because these updates are happening outside of React’s render cycle like we discussed.

"use client";

import { motion, useMotionValue } from "motion/react";

export default function MotionValueBasics() {
const x = useMotionValue(0);

return (
  <div className="wrapper">
    <p>Motion value: {x.get()}</p>
    <motion.div
      style={{ x }}
      className="element"
    />
    <button className="button" onClick={() => x.set(x.get() + 50)}>Increment by 50px</button>
  </div>
);
}

This is nice, but we are here for animations and these updates are instant. Let’s see how we can add some motion to our motion values.


id: framer-motion/hooks-and-animations/animating-motion-values
lesson_id: framer-motion/hooks-and-animations
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: hooks-and-animations
lesson_title: Hooks and animations
heading: Animating motion values
url_path: /lessons/framer-motion/hooks-and-animations/#animating-motion-values
markdown_path: /md/sections/framer-motion/hooks-and-animations/animating-motion-values.md
excerpt:
The useSpring hook creates a motion value that animates to the new value with a spring animation. To make our yellow rectangle animate, we can simply replace useMotionValue with useSpring.

body:
The useSpring hook creates a motion value that animates to the new value with a spring animation. To make our yellow rectangle animate, we can simply replace useMotionValue with useSpring.

const x = useSpring(0);

const x = useSpring(0);

The type of spring can be changed by passing a second argument to the hook. Whenever I use useSpring multiple times in a component, I usually create a variable for the spring values.

const SPRING = {
  type: "spring",
  damping: 10,
  mass: 0.75,
  stiffness: 100,
};

const x = useSpring(0, SPRING);

const SPRING = {
  type: "spring",
  damping: 10,
  mass: 0.75,
  stiffness: 100,
};

const x = useSpring(0, SPRING);

This hook might not feel ground breaking, but let’s think about a component that renders a circle that follows your mouse position. How would you do it with the animate prop?

We would need to add an event listeners for pointermove and a state variable to store the mouse position which would trigger a re-render each time the mouse moves and assign that as the x and y values in the animate prop. This is okay-ish, but the performance is not great.

// This component can re-render even 60 times per second
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });

return (
  <div onPointerMove={(e) => setMousePosition({ x: e.clientX, y: e.clientY })}>
    <motion.div style={{ left: mousePosition.x, top: mousePosition.y }} />
  </div>
)

// This component can re-render even 60 times per second
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });

return (
  <div onPointerMove={(e) => setMousePosition({ x: e.clientX, y: e.clientY })}>
    <motion.div style={{ left: mousePosition.x, top: mousePosition.y }} />
  </div>
)

How would the code for this look like if we were to use motion values? Would it be better or worse? Let’s find out! Your job will be to animate the circle to follow the mouse position using the hooks we’ve learned so far. The end result should look like this:

"use client";

export default function MotionValueBasics() {
return (
  <div className="wrapper">
    <div className="element" />
  </div>
);
}

The interaction above is relatively simple, but the same technique is used in this illustration below that I once made for Linear. It’s basically the same approach, we are following the mouse, the visual does most of the work here to make it look good.

We will build a similar component in the next lesson.

Using useSpring for this type of interactions really does make a difference. You can experience it yourself in the example below, try to toggle the animation on and off. No motion in this case feels lifeless while the spring animation makes it very satisfying.


id: framer-motion/hooks-and-animations/transforming-motion-values
lesson_id: framer-motion/hooks-and-animations
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: hooks-and-animations
lesson_title: Hooks and animations
heading: Transforming motion values
url_path: /lessons/framer-motion/hooks-and-animations/#transforming-motion-values
markdown_path: /md/sections/framer-motion/hooks-and-animations/transforming-motion-values.md
excerpt:
Let’s say we want our circle that follows the cursor to change its size based on the distance from the top of the wrapping div. How can we do that? We have to transform the y motion value. The useTransform hook can help us with that.

body:
Let’s say we want our circle that follows the cursor to change its size based on the distance from the top of the wrapping div. How can we do that? We have to transform the y motion value. The useTransform hook can help us with that.

This hook creates a new motion value that transforms the output of one or more other motion values. Before I dive deeper into explaining how it works, let’s try to implement this effect in our component. We want our circle to scale up to 1.5 when it’s 300px (or more) from top of the screen, when it’s at the top it should be 1.

"use client";

import { motion, useSpring } from "motion/react";

const SPRING = {
mass: 0.1,
damping: 16,
stiffness: 71,
};

export default function MotionValueBasics() {
const x = useSpring(0, SPRING);
const y = useSpring(0, SPRING);
const opacity = useSpring(0);

return (
  <div
    onPointerMove={(e) => {
      const bounds = e.currentTarget.getBoundingClientRect();
      x.set(e.clientX - bounds.left - 24);
      y.set(e.clientY - bounds.top - 24);
    }}
    onPointerEnter={() => opacity.set(1)}
    onPointerLeave={() => opacity.set(0)}
    className="wrapper"
  >
    <motion.div style={{ x, y, opacity }} className="element" />
  </div>
);
}

When I first started using useTransform I remember it as confusing, 2 arrays right after each other, it was a bit hard to wrap my head around the syntax. But that’s why we start with an easy examples, and by no means beautiful examples.

Once you get comfortable with it, you will be able to use it to your advantage often. The Linear illustration I showed you earlier uses useTransform as well. It transforms the distance in pixels to percentages for the offsetDistance property so that the grey and yellow circles follow the mouse.

Another Linear illustration uses useTransform to transform the x value that is based on cursor position into width. The change in width is what makes the bars push each other so that it creates a momentum-like effect.

But a motion value doesn’t necesarily have to be something that you’d use in the style prop. You can also use it to animate numbers for example. In the intro animation below I set the motion value to 45 with a delay. It then interpolates it with a spring animation.

It’s the degree value on the left of the logo, it’s very subtle, but I did that on purpose as there was already a lot happening in this animation. The code for this would look roughly like this:

const angleValue = useSpring(1, {
  stiffness: 185,
  damping: 25,
});

const angleDisplay = useTransform(angleValue,
  value => `${Math.round(value)}°`
);

useEffect(() => {
  const timer = setTimeout(() => {
    angleValue.set(45);
  }, 2600);

  return () => clearTimeout(timer);
}, []);

const angleValue = useSpring(1, {
  stiffness: 185,
  damping: 25,
});

const angleDisplay = useTransform(angleValue,
  value => `${Math.round(value)}°`
);

useEffect(() => {
  const timer = setTimeout(() => {
    angleValue.set(45);
  }, 2600);

  return () => clearTimeout(timer);
}, []);

This introduces a different way of using useTransform. Instead of mapping a motion value, we are transforming the output of it. This is useful as it makes the useTransform hook subscribe to angleValue which is why when we render angleDisplay in our component it would show the up-to-date value unlike a plain useMotionValue hook.


id: framer-motion/hooks-and-animations/when-to-use-which-hook
lesson_id: framer-motion/hooks-and-animations
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: hooks-and-animations
lesson_title: Hooks and animations
heading: When to use which hook?
url_path: /lessons/framer-motion/hooks-and-animations/#when-to-use-which-hook
markdown_path: /md/sections/framer-motion/hooks-and-animations/when-to-use-which-hook.md
excerpt:
You might think that useSpring should be used for all animations because useMotionValue changes are instant. While I personally use useSpring more often, there are definitely cases in which useMotionValue is useful. Here’s one real-world use case.

body:
You might think that useSpring should be used for all animations because useMotionValue changes are instant. While I personally use useSpring more often, there are definitely cases in which useMotionValue is useful. Here’s one real-world use case.

Remember the app store gesture we’ve seen a while ago? Dragging to dismiss also changes the scale of the card. We transform the drag distance into a scale value using useTransform, but we want it to correspond directly to the drag distance. useSpring would feel disconnected from the gesture, because it would animate, so we use useMotionValue to directly update the scale.

useMotionValue becomes useful when working with gestures.


id: framer-motion/hooks-and-animations/practice
lesson_id: framer-motion/hooks-and-animations
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: hooks-and-animations
lesson_title: Hooks and animations
heading: Practice
url_path: /lessons/framer-motion/hooks-and-animations/#practice
markdown_path: /md/sections/framer-motion/hooks-and-animations/practice.md
excerpt:
These are the hooks that I use the most in my work. Let’s now practice them and discover a few additional ones by building some realistic examples.

body:
These are the hooks that I use the most in my work. Let’s now practice them and discover a few additional ones by building some realistic examples.


id: framer-motion/how-do-i-code-animations/layout-animations
lesson_id: framer-motion/how-do-i-code-animations
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: how-do-i-code-animations
lesson_title: How do I code animations
heading: Layout animations
url_path: /lessons/framer-motion/how-do-i-code-animations/#layout-animations
markdown_path: /md/sections/framer-motion/how-do-i-code-animations/layout-animations.md
excerpt:
The most powerful feature of this library is the layout prop. This is what enables our animations to feel native on the web. If we’d want to recreate the App Store’s card animation, we’d have to make the card we clicked on larger to cover the whole screen.

body:
The most powerful feature of this library is the layout prop. This is what enables our animations to feel native on the web. If we’d want to recreate the App Store’s card animation, we’d have to make the card we clicked on larger to cover the whole screen.

But how do we actually make that smooth transition on the web? We can’t animate it to position fixed. We’d probably need to measure the screen’s dimensions and spend a lot of time animating it.

That’s where layout animations come in. They allow us to animate any layout changes easily. Properties that were not possible to animate before, like flex-direction or justify-content, can now be animated smoothly by simply adding the layout prop.

Let’s imagine the yellow box below is the App Store card. It covers the whole screen when you click on it, but there is no transition. Try using Framer Motion to animate it. We are talking about layout animations, so you’ll probably need to use them. Here are the docs for reference.

import { motion } from "motion/react";
import { useState } from "react";

export default function Example() {
const [open, setOpen] = useState(false);

return (
  <div className="wrapper">
    <div
      onClick={() => setOpen((o) => !o)}
      className="element"
	style={
        open
          ? { position: "fixed", inset: 0, width: "100%", height: "100%" }
          : { height: 48, width: 48 }
      }
    />
  </div>
);
}

This is relatively simple to implement, but there is a lot of magic happening under the hood. Because of that, things might not always work as expected in some more complex animations that involve layout changes. You can experience some distortion, or other weird issues.

If you are interested in learning how layout animations work under the hood, you can read Inside Framer’s Magic Motion. It’s an article about reacreating Framer Motion’s layout animations from scratch.


id: framer-motion/how-do-i-code-animations/shared-layout-animations
lesson_id: framer-motion/how-do-i-code-animations
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: how-do-i-code-animations
lesson_title: How do I code animations
heading: Shared layout animations
url_path: /lessons/framer-motion/how-do-i-code-animations/#shared-layout-animations
markdown_path: /md/sections/framer-motion/how-do-i-code-animations/shared-layout-animations.md
excerpt:
Thanks to shared layout animations, we can basically connect two elements and create a smooth transition between them. Below is an example of that. We are not actually moving the element to the center ourselves, the library does it for us.

body:
Thanks to shared layout animations, we can basically connect two elements and create a smooth transition between them. Below is an example of that. We are not actually moving the element to the center ourselves, the library does it for us.


id: framer-motion/how-do-i-code-animations/lost-in-the-mountains
lesson_id: framer-motion/how-do-i-code-animations
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: how-do-i-code-animations
lesson_title: How do I code animations
heading: Lost in the mountains
excerpt: Be careful.
url_path: /lessons/framer-motion/how-do-i-code-animations/#lost-in-the-mountains
markdown_path: /md/sections/framer-motion/how-do-i-code-animations/lost-in-the-mountains.md
body:
Be careful.

We can also use it to make the highlight animation below. The highlight (div with gray color) is essentially rendered for the active element only.

Saved Sites Collections 48 Following 32 Followers

This might be a bit confusing when you first see it so let’s break it down.

{TABS.map((tab) => (
  <motion.li
    layout
    className={clsx(
      "relative cursor-pointer px-2 py-1 text-sm outline-hidden transition-colors",
      activeTab === tab ? "text-gray-800" : "text-gray-700",
    )}
    tabIndex={0}
    key={tab}
    onFocus={() => setActiveTab(tab)}
    onMouseOver={() => setActiveTab(tab)}
    onMouseLeave={() => setActiveTab(tab)}
  >
    {activeTab === tab ? (
      <motion.div
        layoutId="tab-indicator"
        className="absolute inset-0 rounded-lg bg-black/5"
      />
    ) : null}
    <span className="relative text-inherit">{tab}</span>
  </motion.li>
))}

{TABS.map((tab) => (
  <motion.li
    layout
    className={clsx(
      "relative cursor-pointer px-2 py-1 text-sm outline-hidden transition-colors",
      activeTab === tab ? "text-gray-800" : "text-gray-700",
    )}
    tabIndex={0}
    key={tab}
    onFocus={() => setActiveTab(tab)}
    onMouseOver={() => setActiveTab(tab)}
    onMouseLeave={() => setActiveTab(tab)}
  >
    {activeTab === tab ? (
      <motion.div
        layoutId="tab-indicator"
        className="absolute inset-0 rounded-lg bg-black/5"
      />
    ) : null}
    <span className="relative text-inherit">{tab}</span>
  </motion.li>
))}

Here we have two rectangles. When we toggle the showSecond state value, one of them disappears, and the other one appears. Our goal is to have a smooth transition between them. Bonus points if you can get the button to transition smoothly as well, right now it just jumps.

import { motion } from "motion/react";
import { useState } from "react";

export default function Example() {
const [showSecond, setShowSecond] = useState(false);

return (
  <div className="wrapper">
    <button className="button" onClick={() => setShowSecond((s) => !s)}>
      Animate
    </button>
    {showSecond ? (
      <div className="second-element" />
    ) : (
      <div className="element" />
    )}
  </div>
);
}

This is obviously a pretty simple example, but we can already see how powerful this is. Shared layout animations also work when you navigate between pages. It opens so many doors to a more native web.

Let’s try and recreate the animated modals that we have seen before.

Here’s the same component, but without a smooth transition. Given what we just talked about, try to recreate the transition yourself.

"use client";

import { useEffect, useState, useRef } from "react";
import { useOnClickOutside } from "usehooks-ts";
import { motion } from "motion/react";

export default function SharedLayout() {
const [activeGame, setActiveGame] = useState(null);
const ref = useRef(null);
useOnClickOutside(ref, () => setActiveGame(null));

useEffect(() => {
  function onKeyDown(event) {
    if (event.key === "Escape") {
      setActiveGame(null);
    }
  }

  window.addEventListener("keydown", onKeyDown);
  return () => window.removeEventListener("keydown", onKeyDown);
}, []);

return (
  <>
    {activeGame ? (
      <>
        <div className="overlay" />
        <div className="active-game">
          <div className="inner" ref={ref} style={{ borderRadius: 12 }}>
            <div className="header">
              <img
                height={56}
                width={56}
                alt=""
                src={activeGame.image}
                style={{ borderRadius: 12 }}
              />
              <div className="header-inner">
                <div className="content-wrapper">
                  <h2 className="game-title">{activeGame.title}</h2>
                  <p className="game-description">{activeGame.description}</p>
                </div>
                <button className="button">Get</button>
              </div>
            </div>
            <p className="long-description">{activeGame.longDescription}</p>
          </div>
        </div>
      </>
    ) : null}
    <ul className="list">
      {GAMES.map((game) => (
        <li
          key={game.title}
          onClick={() => setActiveGame(game)}
          style={{ borderRadius: 8 }}
        >
          <img
            height={56}
            width={56}
            alt=""
            src={game.image}
            style={{ borderRadius: 12 }}
          />
          <div className="game-wrapper">
            <div className="content-wrapper">
              <h2 className="game-title">{game.title}</h2>
              <p className="game-description">{game.description}</p>
            </div>
            <button className="button">Get</button>
          </div>
        </li>
      ))}
    </ul>
  </>
);
}

const GAMES = [
{
  title: "The Oddysey",
  description: "Explore unknown galaxies.",
  longDescription:
    "Throughout their journey, players will encounter diverse alien races, each with their own unique cultures and technologies. Engage in thrilling space combat, negotiate complex diplomatic relations, and make critical decisions that affect the balance of power in the galaxy.",
  image:
    "https://animations.dev/how-i-use-framer-motion/how-i-code-animations/space.png",
},
{
  title: "Angry Rabbits",
  description: "They are coming for you.",
  longDescription:
    "The rabbits are angry and they are coming for you. You have to defend yourself with your carrot gun. The game is not simple, you have to be fast and accurate to survive.",
  image:
    "https://animations.dev/how-i-use-framer-motion/how-i-code-animations/rabbit.png",
},
{
  title: "Ghost town",
  description: "Find the ghosts.",
  longDescription:
    "You are in a ghost town and you have to find the ghosts. But be careful, they are dangerous.",
  image:
    "https://animations.dev/how-i-use-framer-motion/how-i-code-animations/ghost.webp",
},
{
  title: "Pirates in the jungle",
  description: "Find the treasure.",
  longDescription:
    "You are a pirate and you have to find the treasure in the jungle. But be careful, there are traps and wild animals.",
  image:
    "https://animations.dev/how-i-use-framer-motion/how-i-code-animations/pirate.png",
},

{
  title: "Lost in the mountains",
  description: "Find your way home.",
  longDescription:
    "You are lost in the mountains and you have to find your way home. But be careful, there are dangerous animals and you can get lost.",
  image:
    "https://animations.dev/how-i-use-framer-motion/how-i-code-animations/boy.webp",
},
];


id: framer-motion/how-do-i-code-animations/gestures
lesson_id: framer-motion/how-do-i-code-animations
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: how-do-i-code-animations
lesson_title: How do I code animations
heading: Gestures
url_path: /lessons/framer-motion/how-do-i-code-animations/#gestures
markdown_path: /md/sections/framer-motion/how-do-i-code-animations/gestures.md
excerpt:
Framer Motion exposes a simple yet powerful set of UI gesture recognisers. That means that we can make a draggable component by simply adding the drag prop to a motion element for example.

body:
Framer Motion exposes a simple yet powerful set of UI gesture recognisers. That means that we can make a draggable component by simply adding the drag prop to a motion element for example.

As you can see in the demo above, the draggable element maintains momentum when dragging finishes, which helps it feel more natural. Usually, we want a simple drag functionality though. We can disable this effect by setting dragMomentum to false.

"use client";

import { motion } from "motion/react";
import { useRef } from "react";

export function DragExample() {
  const boundingBox = useRef(null);

  return (
    <div ref={boundingBox} className="h-64 w-full p-6">
      <motion.div
        drag
        // this prevents the element from being dragged outside of the bounding box
        dragConstraints={boundingBox}
		dragMomentum={false}
        className="h-10 w-10 rounded-full bg-gray-400"
      />
    </div>
  );
}

"use client";

import { motion } from "motion/react";
import { useRef } from "react";

export function DragExample() {
  const boundingBox = useRef(null);

  return (
    <div ref={boundingBox} className="h-64 w-full p-6">
      <motion.div
        drag
        // this prevents the element from being dragged outside of the bounding box
        dragConstraints={boundingBox}
		dragMomentum={false}
        className="h-10 w-10 rounded-full bg-gray-400"
      />
    </div>
  );
}


id: framer-motion/how-do-i-code-animations/app-store-like-transition
lesson_id: framer-motion/how-do-i-code-animations
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: how-do-i-code-animations
lesson_title: How do I code animations
heading: App Store-like transition
url_path: /lessons/framer-motion/how-do-i-code-animations/#app-store-like-transition
markdown_path: /md/sections/framer-motion/how-do-i-code-animations/app-store-like-transition.md
excerpt:
Initially, this component was a separate lesson. But, I realized that it’s very similar to the shared layout exercise we did before. The design is just different. We will cover a Feedback popover instead, which isn’t as similar to the shared layout exercise and touches a few interesting topics.

body:
Initially, this component was a separate lesson. But, I realized that it’s very similar to the shared layout exercise we did before. The design is just different. We will cover a Feedback popover instead, which isn’t as similar to the shared layout exercise and touches a few interesting topics.

Keep in mind that it’s not finished, there is a small text jump when you click on the card and in dark mode, there’s a slight white border present when animating. I might come back to it in the future and cover it more in-depth.


id: framer-motion/how-do-i-code-animations/game-of-the-day
lesson_id: framer-motion/how-do-i-code-animations
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: how-do-i-code-animations
lesson_title: How do I code animations
heading: Game of the day
excerpt: A game about vikings
url_path: /lessons/framer-motion/how-do-i-code-animations/#game-of-the-day
markdown_path: /md/sections/framer-motion/how-do-i-code-animations/game-of-the-day.md
body:
A game about vikings

Are you ready? A game about vikings, where you can play as a viking and fight other vikings. You can also build your own viking village and explore the world.

The never ending adventureIn this game set in a fairy tale world, players embark on a quest through mystical lands filled with enchanting forests and towering mountains.

I did prepare the code for the App Store-like transition, so if you want to try it out, it’s right below. We won’t do an explanation video on this one, it’s just pure code. You can see it as an extra exercise. Try reading through the code and understanding it. If you have any questions, feel free to email me, I’m always happy to help.

Keep in mind that the drag interaction is not added here yet due to time constraints. I’ll add it in the near future though!

"use client";

import { useState, useEffect, useRef } from "react";
import { AnimatePresence, motion } from "motion/react";
import "./styles.css";
import { useOnClickOutside } from "usehooks-ts";

function Card({ card, setActiveCard }) {
return (
  <motion.div
    layoutId={`card-${card.title}`}
    className="card"
    whileTap={{ scale: 0.98 }}
    onClick={() => setActiveCard(card)}
    style={{ borderRadius: 20 }}
  >
    <motion.img
      layoutId={`image-${card.title}`}
      src={card.image}
      alt=""
      style={{ borderRadius: 20 }}
    />
    <motion.button
      aria-hidden
      tabIndex={-1}
	layoutId={`close-button-${card.title}`}
      className="close-button"
      style={{ opacity: 0 }}
    >
      <svg
        xmlns="http://www.w3.org/2000/svg"
        fill="none"
        viewBox="0 0 24 24"
        strokeWidth="2"
        height="20"
        width="20"
        stroke="currentColor"
      >
        <path
          strokeLinecap="round"
          strokeLinejoin="round"
          d="M6 18 18 6M6 6l12 12"
        />
      </svg>
    </motion.button>
    <motion.div
      layoutId={`card-content-${card.title}`}
      className="card-content"
    >
      <div className="card-text">
        <motion.h2
          layoutId={`card-heading-${card.title}`}
          className="card-heading"
        >
          Game of the day
        </motion.h2>
      </div>
      <motion.div
        layoutId={`card-extra-info-${card.title}`}
        className="extra-info"
        style={{ borderBottomLeftRadius: 20, borderBottomRightRadius: 20 }}
      >
        <motion.img
          src={card.logo}
          width={40}
          height={40}
          alt=""
          layoutId={`card-game-image-${card.title}`}
          className="rounded-lg"
        />
        <div className="desc-wrapper">
          <motion.span
            layoutId={`card-game-title-${card.title}`}
            className="game-title"
          >
            {card.title}
          </motion.span>
          <motion.span
            layoutId={`card-game-subtitle-${card.title}`}
            className="game-subtitle"
          >
            {card.description}
          </motion.span>
        </div>
        <motion.button
          layoutId={`card-button-${card.title}`}
          className="get-button"
        >
          Get
        </motion.button>
      </motion.div>
    </motion.div>

    <motion.div
      layoutId={`card-long-description-${card.title}`}
      className="long-description"
      style={{ position: "absolute", top: "100%", opacity: 0 }}
    >
      <div>
        <p>
          <b>Are you ready?</b> {card.longDescription}
        </p>
        <p>
          <b>The never ending adventure</b>
          In this game set in a fairy tale world, players embark on a quest
          through mystical lands filled with enchanting forests and towering
          mountains.
        </p>
      </div>
    </motion.div>
  </motion.div>
);
}

function ActiveCard({ activeCard, setActiveCard }) {
const ref = useRef(null);
useOnClickOutside(ref, () => setActiveCard(null));

return (
  <motion.div
    ref={ref}
    layoutId={`card-${activeCard.title}`}
    className="card card-active"
    style={{ borderRadius: 0 }}
  >
    <div className="card-inner">
      <motion.img
        layoutId={`image-${activeCard.title}`}
        src={activeCard.image}
        alt=""
        style={{ borderRadius: 0 }}
      />
      <motion.button
        layoutId={`close-button-${activeCard.title}`}
        className="close-button"
        aria-label="Close button"
        onClick={() => setActiveCard(null)}
      >
        <svg
          xmlns="http://www.w3.org/2000/svg"
          fill="none"
          viewBox="0 0 24 24"
          strokeWidth="2"
          height="20"
          width="20"
          stroke="currentColor"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            d="M6 18 18 6M6 6l12 12"
          />
        </svg>
      </motion.button>
      <motion.div
        layoutId={`card-content-${activeCard.title}`}
        className="card-content active-card-content"
      >
        <div className="card-text">
          <motion.h2
            layoutId={`card-heading-${activeCard.title}`}
            layout
            className="card-heading"
          >
            Game of the day
          </motion.h2>
        </div>
        <motion.div
          layoutId={`card-extra-info-${activeCard.title}`}
          className="extra-info"
          style={{ borderBottomLeftRadius: 20, borderBottomRightRadius: 20 }}
        >
          <motion.img
            src={activeCard.logo}
            width={40}
            height={40}
            alt=""
            layoutId={`card-game-image-${activeCard.title}`}
            className="rounded-lg"
          />
          <div className="desc-wrapper">
            <motion.span
              layoutId={`card-game-title-${activeCard.title}`}
              className="game-title"
            >
              {activeCard.title}
            </motion.span>
            <motion.span
              layoutId={`card-game-subtitle-${activeCard.title}`}
              className="game-subtitle"
            >
              {activeCard.description}
            </motion.span>
          </div>
          <motion.button
            layoutId={`card-button-${activeCard.title}`}
            layout
            className="get-button"
          >
            Get
          </motion.button>
        </motion.div>
      </motion.div>
    </div>

    <motion.div
      layoutId={`card-long-description-${activeCard.title}`}
      className="long-description"
    >
      <p>
        <b>Are you ready?</b> {activeCard.longDescription}
      </p>
      <p>
        <b>The never ending adventure </b>
        In this game set in a fairy tale world, players embark on a quest
        through mystical lands filled with enchanting forests and towering
        mountains. Players can explore the world, build their own viking
      </p>
    </motion.div>
  </motion.div>
);
}

export default function StyledWithoutDrag() {
const [activeCard, setActiveCard] = useState(null);

useEffect(() => {
  function onKeyDown(event) {
    if (event.key === "Escape") {
      setActiveCard(null);
    }
  }

  window.addEventListener("keydown", onKeyDown);
  return () => window.removeEventListener("keydown", onKeyDown);
}, []);

return (
  <div className="cards-wrapper">
    {CARDS.map((card) => (
      <Card key={card.title} card={card} setActiveCard={setActiveCard} />
    ))}
    <AnimatePresence>
      {activeCard ? (
        <motion.div
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          className="overlay"
        />
      ) : null}
    </AnimatePresence>
    <AnimatePresence>
      {activeCard ? (
        <ActiveCard activeCard={activeCard} setActiveCard={setActiveCard} />
      ) : null}
    </AnimatePresence>
  </div>
);
}

const CARDS = [
{
  title: "Vikings",
  subtitle: "Clash of the Norse Warriors",
  description: "A game about vikings",
  longDescription:
    "A game about vikings, where you can play as a viking and fight other vikings. You can also build your own viking village and explore the world.",
  image:
    "https://animations.dev/how-i-use-framer-motion/app-store-like-cards/game.webp",
  logo: "https://animations.dev/how-i-use-framer-motion/app-store-like-cards/game-logo.webp",
},
];


id: framer-motion/how-do-i-code-animations/that-s-it
lesson_id: framer-motion/how-do-i-code-animations
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: how-do-i-code-animations
lesson_title: How do I code animations
heading: That’s it
url_path: /lessons/framer-motion/how-do-i-code-animations/#that-s-it
markdown_path: /md/sections/framer-motion/how-do-i-code-animations/that-s-it.md
excerpt:
These are the features of Framer Motion that I use the most. When you see one of my animations posted on Twitter, there’s a high chance it was done using the features we talked about in this lesson.

body:
These are the features of Framer Motion that I use the most. When you see one of my animations posted on Twitter, there’s a high chance it was done using the features we talked about in this lesson.

There are more features in Framer Motion, like reorder, useAnimate, etc. We will touch on some of them in the upcoming lessons. But in this module we talk about how I use Framer Motion, and I simply don’t use everything it offers. So we’ll focus on the features I think are the most useful.


id: framer-motion/interactive-graph/making-it-interactive
lesson_id: framer-motion/interactive-graph
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: interactive-graph
lesson_title: Interactive graph
heading: Making it interactive
url_path: /lessons/framer-motion/interactive-graph/#making-it-interactive
markdown_path: /md/sections/framer-motion/interactive-graph/making-it-interactive.md
excerpt:
Before we add any motion, we first need to make the graph follow the cursor. The end result should look like this:

body:
Before we add any motion, we first need to make the graph follow the cursor. The end result should look like this:

Our starting point will be a simple static svg graph. This exercise will cover what we just learned in the last lesson, but also a few techniques we have learned earlier in the course, and even a new Framer Motion hook. You can use the hints below if you get stuck. And remember, it’s okay to struggle, that’s how you learn. Good luck!

"use client";

export default function Graph() {
return (
  <div className="wrapper">
    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 644 188">
      <path
        stroke="#0090FF"
        strokeWidth="2"
        d="M1 118.5s82.308-15.501 113.735-29 74.769-1.713 121.217-12c37.596-8.328 58.517-15.006 93.781-30.5 80.146-35.215 123.213-16 154.141-24.5S635.97.849 644 1.5"
      ></path>
      <path
        fill="url(#paint0_linear_540_31)"
        d="M113.912 89.012C82.437 102.511 1 118.01 1 118.01V188h643V1.023c-8.043-.65-129.399 12.499-160.375 20.998-30.976 8.498-74.11-10.714-154.38 24.496-35.319 15.493-56.272 22.17-93.927 30.497-46.52 10.286-89.93-1.5-121.406 11.998"
      ></path>
      <defs>
        <linearGradient
          id="paint0_linear_540_31"
          x1="322.5"
          x2="322.5"
          y1="1"
          y2="188"
          gradientUnits="userSpaceOnUse"
        >
          <stop stopColor="#138EED" stopOpacity="0.4"></stop>
          <stop offset="1" stopColor="#058FFB" stopOpacity="0"></stop>
        </linearGradient>
      </defs>
    </svg>
  </div>
);
}


id: framer-motion/interactive-graph/adding-motion
lesson_id: framer-motion/interactive-graph
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: interactive-graph
lesson_title: Interactive graph
heading: Adding motion
url_path: /lessons/framer-motion/interactive-graph/#adding-motion
markdown_path: /md/sections/framer-motion/interactive-graph/adding-motion.md
excerpt:
The end result should look like the component below, pay attention to not only the motion, but also one detail that makes the graph feel nicer.

body:
The end result should look like the component below, pay attention to not only the motion, but also one detail that makes the graph feel nicer.

"use client";

import { useMotionValue, useMotionTemplate, motion } from "motion/react";

export default function Graph() {
const clipPathValue = useMotionValue(0);
const clipPathTemplate = useMotionTemplate`inset(0px ${clipPathValue}% 0px 0px)`;

function onPointerMove(e) {
  const rect = e.currentTarget.getBoundingClientRect();
  const distanceFromRight = Math.max(rect.right - e.clientX, 0);
  const percentageFromRight = Math.min(
    (distanceFromRight / rect.width) * 100,
    100,
  );
  clipPathValue.set(percentageFromRight);
}

return (
  <div className="wrapper" onPointerMove={onPointerMove}>
    <motion.svg
      xmlns="http://www.w3.org/2000/svg"
      fill="none"
      viewBox="0 0 644 188"
      onPointerMove={onPointerMove}
      style={{
        clipPath: clipPathTemplate,
      }}
    >
      <path
        stroke="#0090FF"
        strokeWidth="2"
        d="M1 118.5s82.308-15.501 113.735-29 74.769-1.713 121.217-12c37.596-8.328 58.517-15.006 93.781-30.5 80.146-35.215 123.213-16 154.141-24.5S635.97.849 644 1.5"
      ></path>
      <path
        fill="url(#paint0_linear_540_31)"
        d="M113.912 89.012C82.437 102.511 1 118.01 1 118.01V188h643V1.023c-8.043-.65-129.399 12.499-160.375 20.998-30.976 8.498-74.11-10.714-154.38 24.496-35.319 15.493-56.272 22.17-93.927 30.497-46.52 10.286-89.93-1.5-121.406 11.998"
      ></path>
      <defs>
        <linearGradient
          id="paint0_linear_540_31"
          x1="322.5"
          x2="322.5"
          y1="1"
          y2="188"
          gradientUnits="userSpaceOnUse"
        >
          <stop stopColor="#138EED" stopOpacity="0.4"></stop>
          <stop offset="1" stopColor="#058FFB" stopOpacity="0"></stop>
        </linearGradient>
      </defs>
    </motion.svg>
  </div>
);
}


id: framer-motion/multi-step-component/step-animation
lesson_id: framer-motion/multi-step-component
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: multi-step-component
lesson_title: Multi-step component
heading: Step animation
excerpt: Let’s try animating the steps first, with no height animation or direction awareness.
url_path: /lessons/framer-motion/multi-step-component/#step-animation
markdown_path: /md/sections/framer-motion/multi-step-component/step-animation.md
body:
Let’s try animating the steps first, with no height animation or direction awareness.

Success criteria:

When the continue or back button is pressed, the exiting step should animate to the left and the entering step should animate from the right. The exit animation should consist of opacity and x transition.

"use client";

import { useMemo, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import useMeasure from "react-use-measure";
import "./styles.css";

export default function MultiStepComponent() {
const [currentStep, setCurrentStep] = useState(0);

const content = useMemo(() => {
  switch (currentStep) {
    case 0:
      return (
        <>
          <h2 className="heading">This is step one</h2>
          <p>
            Usually in this step we would explain why this thing exists and
            what it does. Also, we would show a button to go to the next step.
          </p>
          <div className="skeletons">
            <div className="skeleton" style={{ width: 256 }} />
            <div className="skeleton" style={{ width: 192 }} />
            <div className="skeleton" />
            <div className="skeleton" style={{ width: 384 }} />
          </div>
        </>
      );
    case 1:
      return (
        <>
          <h2 className="heading">This is step two</h2>
          <p>
            Usually in this step we would explain why this thing exists and
            what it does. Also, we would show a button to go to the next step.
          </p>
          <div className="skeletons">
            <div className="skeleton" style={{ width: 256 }} />
            <div className="skeleton" style={{ width: 192 }} />
            <div className="skeleton" style={{ width: 384 }} />
          </div>
        </>
      );
    case 2:
      return (
        <>
          <h2 className="heading">This is step three</h2>
          <p>
            Usually in this step we would explain why this thing exists and
            what it does. Also, we would show a button to go to the next step.
          </p>
          <div className="skeletons">
            <div className="skeleton" style={{ width: 256 }} />
            <div className="skeleton" style={{ width: 192 }} />
            <div className="skeleton" style={{ width: 128 }} />
            <div className="skeleton" style={{ width: 224 }} />
            <div className="skeleton" style={{ width: 384 }} />
          </div>
        </>
      );
  }
}, [currentStep]);

return (
  <div className="multi-step-wrapper">
    <div className="multi-step-inner">
      <div>{content}</div>
      <div className="actions">
        <button
          className="secondary-button"
          disabled={currentStep === 0}
          onClick={() => {
            if (currentStep === 0) {
              return;
            }
            setCurrentStep((prev) => prev - 1);
          }}
        >
          Back
        </button>
        <button
          className="primary-button"
          disabled={currentStep === 2}
          onClick={() => {
            if (currentStep === 2) {
              setCurrentStep(0);
              return;
            }
            setCurrentStep((prev) => prev + 1);
          }}
        >
          Continue
        </button>
      </div>
    </div>
  </div>
);
}


id: framer-motion/multi-step-component/height-animation
lesson_id: framer-motion/multi-step-component
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: multi-step-component
lesson_title: Multi-step component
heading: Height animation
excerpt: Animating the height will make our component look less jarring. Let’s try that next.
url_path: /lessons/framer-motion/multi-step-component/#height-animation
markdown_path: /md/sections/framer-motion/multi-step-component/height-animation.md
body:
Animating the height will make our component look less jarring. Let’s try that next.

Success criteria:

The height should animate smoothly when the content changes, it should not use magic numbers. The buttons should follow the height animation, they should not jump around.

"use client";

import { useMemo, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import useMeasure from "react-use-measure";
import "./styles.css";

export default function MultiStepComponent() {
const [currentStep, setCurrentStep] = useState(0);

const content = useMemo(() => {
  switch (currentStep) {
    case 0:
      return (
        <>
          <h2 className="heading">This is step one</h2>
          <p>
            Usually in this step we would explain why this thing exists and
            what it does. Also, we would show a button to go to the next step.
          </p>
          <div className="skeletons">
            <div className="skeleton" style={{ width: 256 }} />
            <div className="skeleton" style={{ width: 192 }} />
            <div className="skeleton" />
            <div className="skeleton" style={{ width: 384 }} />
          </div>
        </>
      );
    case 1:
      return (
        <>
          <h2 className="heading">This is step two</h2>
          <p>
            Usually in this step we would explain why this thing exists and
            what it does. Also, we would show a button to go to the next step.
          </p>
          <div className="skeletons">
            <div className="skeleton" style={{ width: 256 }} />
            <div className="skeleton" style={{ width: 192 }} />
            <div className="skeleton" style={{ width: 384 }} />
          </div>
        </>
      );
    case 2:
      return (
        <>
          <h2 className="heading">This is step three</h2>
          <p>
            Usually in this step we would explain why this thing exists and
            what it does. Also, we would show a button to go to the next step.
          </p>
          <div className="skeletons">
            <div className="skeleton" style={{ width: 256 }} />
            <div className="skeleton" style={{ width: 192 }} />
            <div className="skeleton" style={{ width: 128 }} />
            <div className="skeleton" style={{ width: 224 }} />
            <div className="skeleton" style={{ width: 384 }} />
          </div>
        </>
      );
  }
}, [currentStep]);

return (
  <div className="multi-step-wrapper">
    <div className="multi-step-inner">
      <AnimatePresence mode="popLayout" initial={false}>
        <motion.div
          key={currentStep}
          initial={{ x: "110%", opacity: 0 }}
          animate={{ opacity: 1, x: 0 }}
          exit={{ x: "-110%", opacity: 0 }}
          transition={{ duration: 0.5, type: "spring", bounce: 0 }}
        >
          {content}
        </motion.div>
      </AnimatePresence>
      <div className="actions">
        <button
          className="secondary-button"
          disabled={currentStep === 0}
          onClick={() => {
            if (currentStep === 0) {
              return;
            }
            setCurrentStep((prev) => prev - 1);
          }}
        >
          Back
        </button>
        <button
          className="primary-button"
          disabled={currentStep === 2}
          onClick={() => {
            if (currentStep === 2) {
              setCurrentStep(0);
              return;
            }
            setCurrentStep((prev) => prev + 1);
          }}
        >
          Continue
        </button>
      </div>
    </div>
  </div>
);
}


id: framer-motion/multi-step-component/direction-awareness
lesson_id: framer-motion/multi-step-component
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: multi-step-component
lesson_title: Multi-step component
heading: Direction awareness
url_path: /lessons/framer-motion/multi-step-component/#direction-awareness
markdown_path: /md/sections/framer-motion/multi-step-component/direction-awareness.md
excerpt:
Currently, our elements enter from the right and disappear to the left, regardless of whether we are moving forward or backward. We want to flip our x-values depending on the direction we are moving towards. Good luck!

body:
Currently, our elements enter from the right and disappear to the left, regardless of whether we are moving forward or backward. We want to flip our x-values depending on the direction we are moving towards. Good luck!

Success criteria:

When the continue button is pressed, the entering step should animate from the right and the exiting step should animate to the left. When the back button is pressed, the entering step should animate from the left and the exiting step should animate to the right.

"use client";

import { useMemo, useState } from "react";
import { AnimatePresence, motion, MotionConfig } from "motion/react";
import useMeasure from "react-use-measure";
import "./styles.css";

export default function MultiStepComponent() {
const [currentStep, setCurrentStep] = useState(0);
const [ref, bounds] = useMeasure();

const content = useMemo(() => {
  switch (currentStep) {
    case 0:
      return (
        <>
          <h2 className="heading">This is step one</h2>
          <p>
            Usually in this step we would explain why this thing exists and
            what it does. Also, we would show a button to go to the next step.
          </p>
          <div className="skeletons">
            <div className="skeleton" style={{ width: 256 }} />
            <div className="skeleton" style={{ width: 192 }} />
            <div className="skeleton" />
            <div className="skeleton" style={{ width: 384 }} />
          </div>
        </>
      );
    case 1:
      return (
        <>
          <h2 className="heading">This is step two</h2>
          <p>
            Usually in this step we would explain why this thing exists and
            what it does. Also, we would show a button to go to the next step.
          </p>
          <div className="skeletons">
            <div className="skeleton" style={{ width: 256 }} />
            <div className="skeleton" style={{ width: 192 }} />
            <div className="skeleton" style={{ width: 384 }} />
          </div>
        </>
      );
    case 2:
      return (
        <>
          <h2 className="heading">This is step three</h2>
          <p>
            Usually in this step we would explain why this thing exists and
            what it does. Also, we would show a button to go to the next step.
          </p>
          <div className="skeletons">
            <div className="skeleton" style={{ width: 256 }} />
            <div className="skeleton" style={{ width: 192 }} />
            <div className="skeleton" style={{ width: 128 }} />
            <div className="skeleton" style={{ width: 224 }} />
            <div className="skeleton" style={{ width: 384 }} />
          </div>
        </>
      );
  }
}, [currentStep]);

return (
  <MotionConfig transition={{ duration: 0.5, type: "spring", bounce: 0 }}>
    <motion.div
      animate={{ height: bounds.height }}
      className="multi-step-wrapper"
    >
      <div className="multi-step-inner" ref={ref}>
        <AnimatePresence mode="popLayout" initial={false}>
          <motion.div
            key={currentStep}
            initial={{ x: "110%", opacity: 0 }}
            animate={{ opacity: 1, x: 0 }}
            exit={{ x: "-110%", opacity: 0 }}
          >
            {content}
          </motion.div>
        </AnimatePresence>
        <motion.div layout className="actions">
          <button
            className="secondary-button"
            disabled={currentStep === 0}
            onClick={() => {
              if (currentStep === 0) {
                return;
              }
              setCurrentStep((prev) => prev - 1);
            }}
          >
            Back
          </button>
          <button
            className="primary-button"
            disabled={currentStep === 2}
            onClick={() => {
              if (currentStep === 2) {
                setCurrentStep(0);
                return;
              }
              setCurrentStep((prev) => prev + 1);
            }}
          >
            Continue
          </button>
        </motion.div>
      </div>
    </motion.div>
  </MotionConfig>
);
}


id: framer-motion/multi-step-component/accessibility
lesson_id: framer-motion/multi-step-component
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: multi-step-component
lesson_title: Multi-step component
heading: Accessibility
url_path: /lessons/framer-motion/multi-step-component/#accessibility
markdown_path: /md/sections/framer-motion/multi-step-component/accessibility.md
excerpt:
There is quite a lot movement happening on the screen. To disable the animation for people that have reduced motion enabled, we can use the useReducedMotion hook from Framer Motion. We haven’t done it in this exercise, as I wanted to keep the code relatively simple, but we we’ll talk about it in the accessibility lesson.

body:
There is quite a lot movement happening on the screen. To disable the animation for people that have reduced motion enabled, we can use the useReducedMotion hook from Framer Motion. We haven’t done it in this exercise, as I wanted to keep the code relatively simple, but we we’ll talk about it in the accessibility lesson.


id: framer-motion/multi-step-component/key-takeaways
lesson_id: framer-motion/multi-step-component
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: multi-step-component
lesson_title: Multi-step component
heading: Key takeaways
url_path: /lessons/framer-motion/multi-step-component/#key-takeaways
markdown_path: /md/sections/framer-motion/multi-step-component/key-takeaways.md
excerpt:
When an element is animating out using AnimatePresence its state is stale. To work around this, you can use the custom prop on our motion.div as well as on AnimatePresence. This will ensure that all leaving components are using the latest data.

body:
When an element is animating out using AnimatePresence its state is stale. To work around this, you can use the custom prop on our motion.div as well as on AnimatePresence. This will ensure that all leaving components are using the latest data.


id: framer-motion/multi-step-component/rapid-switching
lesson_id: framer-motion/multi-step-component
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: multi-step-component
lesson_title: Multi-step component
heading: Rapid switching
url_path: /lessons/framer-motion/multi-step-component/#rapid-switching
markdown_path: /md/sections/framer-motion/multi-step-component/rapid-switching.md
excerpt:
When you switch quickly between states in animate presence, you might eventually see both elements being visible in the DOM. That’s a bug in Framer Motion that will hopefully be fixed. If you are experiencing it and you can’t work around it, I suggest installing version 11.0.10 of Framer Motion where this bug is not present.

body:
When you switch quickly between states in animate presence, you might eventually see both elements being visible in the DOM. That’s a bug in Framer Motion that will hopefully be fixed. If you are experiencing it and you can’t work around it, I suggest installing version 11.0.10 of Framer Motion where this bug is not present.


id: framer-motion/multi-step-component/inspiration
lesson_id: framer-motion/multi-step-component
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: multi-step-component
lesson_title: Multi-step component
heading: Inspiration
url_path: /lessons/framer-motion/multi-step-component/#inspiration
markdown_path: /md/sections/framer-motion/multi-step-component/inspiration.md
excerpt:
This exercise is inspired by buildui.com where Sam Selikoff talks about a similar problem. I’m a huge admirer of his work and I highly recommend checking out his content.

body:
This exercise is inspired by buildui.com where Sam Selikoff talks about a similar problem. I’m a huge admirer of his work and I highly recommend checking out his content.


id: framer-motion/the-basics/anatomy-of-an-animation
lesson_id: framer-motion/the-basics
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: the-basics
lesson_title: The Basics
heading: Anatomy of an animation
url_path: /lessons/framer-motion/the-basics/#anatomy-of-an-animation
markdown_path: /md/sections/framer-motion/the-basics/anatomy-of-an-animation.md
excerpt:
To animate with Framer Motion we need to use the motion element. It’s a wrapper around the native HTML elements that allows us to animate them using Framer Motion’s API.

body:
To animate with Framer Motion we need to use the motion element. It’s a wrapper around the native HTML elements that allows us to animate them using Framer Motion’s API.


id: framer-motion/the-basics/framer-motion-imports
lesson_id: framer-motion/the-basics
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: the-basics
lesson_title: The Basics
heading: Framer Motion imports
url_path: /lessons/framer-motion/the-basics/#framer-motion-imports
markdown_path: /md/sections/framer-motion/the-basics/framer-motion-imports.md
excerpt:
Framer Motion is now Motion for React. We still use motion imports in this course, but this doesn’t change much, the API of the library is the same. The only difference is the import path. You can read more about it here.

body:
Framer Motion is now Motion for React. We still use motion imports in this course, but this doesn’t change much, the API of the library is the same. The only difference is the import path. You can read more about it here.

import { motion } from "motion/react";
import { useState } from "react";

export default function Example() {

return (
<div className="wrapper">
    <motion.div className="element" />
</div>
)
}

When working with animations you have a start and an end state. In Framer Motion, you can define these states using the initial and animate props. The initial prop defines the starting state of the animation, while the animate prop defines the end state.

Let’s say we want to create an enter animation for our yellow rectangle. We can define our initial state as follows:

<motion.div
  initial={{ opacity: 0, scale: 0 }}
  className="element"
/>

<motion.div
  initial={{ opacity: 0, scale: 0 }}
  className="element"
/>

This will make our element invisible when it renders the first time (at mount time). To define our end state, we simply add an animate prop, and define the properties we want to end up with:

<motion.div
  initial={{ opacity: 0, scale: 0 }}
  animate={{ opacity: 1, scale: 1 }}
  className="element"
/>

<motion.div
  initial={{ opacity: 0, scale: 0 }}
  animate={{ opacity: 1, scale: 1 }}
  className="element"
/>

To view our effect, reload the sandbox by pressing on the refresh button in the top right corner.

import { motion } from "motion/react";
import { useState } from "react";

export default function Example() {
return (
<div className="wrapper">
    <motion.div
      initial={{ opacity: 0, scale: 0 }}
      animate={{ opacity: 1, scale: 1 }}
      className="element"
    />
</div>
)
}

When we inspect a Framer Motion animation we can see that the value we provided isn’t immediately applied. It’s interpolated, which means that the value is gradually changed. That’s because the animation is done in Javascript and not CSS.

This is not the element above, but it’s a similar example.

The animation happens outside of React’s render cycle, so every time the animation updates, it doesn’t trigger a re-render, which is great for performance.


id: framer-motion/the-basics/transition-prop
lesson_id: framer-motion/the-basics
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: the-basics
lesson_title: The Basics
heading: Transition prop
url_path: /lessons/framer-motion/the-basics/#transition-prop
markdown_path: /md/sections/framer-motion/the-basics/transition-prop.md
excerpt:
By default, Framer Motion will create an appropriate animation for a snappy transition based on the types of value being animated. For instance, physical properties like x or scale will be animated via a spring simulation. Whereas values like opacity or color will be animated with a tween (easing-based).

body:
By default, Framer Motion will create an appropriate animation for a snappy transition based on the types of value being animated. For instance, physical properties like x or scale will be animated via a spring simulation. Whereas values like opacity or color will be animated with a tween (easing-based).

We can also define our own transition using the transition prop. It takes in an object with properties like duration, type, delay, and more.

You can see both of the animation types below.

// Spring animation
<motion.div
  animate={{
    x: 100,
    transition: { type: "spring", duration: 0.5, bounce: 0.2 },
  }}
/>

// Easing animation
<motion.div
  animate={{
    x: 100,
    transition: { duration: 0.3, ease: "easeOut" },
  }}
/>

// Spring animation
<motion.div
  animate={{
    x: 100,
    transition: { type: "spring", duration: 0.5, bounce: 0.2 },
  }}
/>

// Easing animation
<motion.div
  animate={{
    x: 100,
    transition: { duration: 0.3, ease: "easeOut" },
  }}
/>


id: framer-motion/the-basics/exit-animations
lesson_id: framer-motion/the-basics
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: the-basics
lesson_title: The Basics
heading: Exit animations
url_path: /lessons/framer-motion/the-basics/#exit-animations
markdown_path: /md/sections/framer-motion/the-basics/exit-animations.md
excerpt:
Exit animations in React are hard. AnimatePresence in Framer Motion allows components to animate out when they’re removed from the React tree.

body:
Exit animations in React are hard. AnimatePresence in Framer Motion allows components to animate out when they’re removed from the React tree.

It has good DX as well. All you need to do is wrap an element you want to animate out with AnimatePresence, and add the exit prop, which works the same way as initial and animate, except it defines the end state of our component when it’s removed.

import { motion, AnimatePresence } from "motion/react"

export const MyComponent = ({ isVisible }) => (
  <AnimatePresence>
    {isVisible ? (
      <motion.div
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        exit={{ opacity: 0 }}
      />
    ) : null}
  </AnimatePresence>
)

import { motion, AnimatePresence } from "motion/react"

export const MyComponent = ({ isVisible }) => (
  <AnimatePresence>
    {isVisible ? (
      <motion.div
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        exit={{ opacity: 0 }}
      />
    ) : null}
  </AnimatePresence>
)

It also has different modes. I often use the wait mode to animate between two elements. The copy button we’ve seen earlier in this lesson is using this mode. When you click the button, the copy icon animates out, and only after that, the checkmark animates in.

const variants = {
  hidden: { opacity: 0, scale: 0.5 },
  visible: { opacity: 1, scale: 1 },
};

// ...

<button aria-label="Copy code snippet" onClick={copy}>
  <AnimatePresence mode="wait" initial={false}>
    {copied ? (
      <motion.span
        key="checkmark"
        variants={variants}
        initial="hidden"
        animate="visible"
        exit="hidden"
      >
        <CheckmarkIcon />
      </motion.span>
    ) : (
      <motion.span
        key="copy"
        variants={variants}
        initial="hidden"
        animate="visible"
        exit="hidden"
      >
        <CopyIcon />
      </motion.span>
    )}
  </AnimatePresence>
</button>

const variants = {
  hidden: { opacity: 0, scale: 0.5 },
  visible: { opacity: 1, scale: 1 },
};

// ...

<button aria-label="Copy code snippet" onClick={copy}>
  <AnimatePresence mode="wait" initial={false}>
    {copied ? (
      <motion.span
        key="checkmark"
        variants={variants}
        initial="hidden"
        animate="visible"
        exit="hidden"
      >
        <CheckmarkIcon />
      </motion.span>
    ) : (
      <motion.span
        key="copy"
        variants={variants}
        initial="hidden"
        animate="visible"
        exit="hidden"
      >
        <CopyIcon />
      </motion.span>
    )}
  </AnimatePresence>
</button>

With this type of animations, it’s important to include initial={false} on AnimatePresence. This tells Framer Motion not to animate on the initial render.


id: framer-motion/the-basics/tip
lesson_id: framer-motion/the-basics
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: the-basics
lesson_title: The Basics
heading: Tip
url_path: /lessons/framer-motion/the-basics/#tip
markdown_path: /md/sections/framer-motion/the-basics/tip.md
excerpt:
If an animation that involves AnimatePresence is not working as expected, make sure that you have a key prop on the element you’re animating. Otherwise, the component won’t be unmounted and the exit animation won’t be triggered.

body:
If an animation that involves AnimatePresence is not working as expected, make sure that you have a key prop on the element you’re animating. Otherwise, the component won’t be unmounted and the exit animation won’t be triggered.

AnimatePresence is also the key for this button animation, which is used on the login page of this course.

Let’s try and recreate it.

"use client";

import { useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import { Spinner } from "./Spinner";
import "./styles.css";

const buttonCopy = {
idle: "Send me a login link",
loading: <Spinner size={16} color="rgba(255, 255, 255, 0.65)" />,
success: "Login link sent!",
};

export default function SmoothButton() {
const [buttonState, setButtonState] = useState("idle");

return (
  <div className="outer-wrapper">
    <button
      className="blue-button"
      disabled={buttonState !== "idle"}
      onClick={() => {
        // This code is just a placeholder
        setButtonState("loading");

        setTimeout(() => {
          setButtonState("success");
        }, 1750);

        setTimeout(() => {
          setButtonState("idle");
        }, 3500);
      }}
    >
      <span>{buttonCopy[buttonState]}</span>
    </button>
  </div>
);
}


id: framer-motion/the-basics/variants
lesson_id: framer-motion/the-basics
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: the-basics
lesson_title: The Basics
heading: Variants
url_path: /lessons/framer-motion/the-basics/#variants
markdown_path: /md/sections/framer-motion/the-basics/variants.md
excerpt:
We can also make a reusable component out of this, so that it doesn’t have to be used only for this button. Here, I also used variants. Variants are predefined sets of targets which can be then used in the initial, animate, and exit props.

body:
We can also make a reusable component out of this, so that it doesn’t have to be used only for this button. Here, I also used variants. Variants are predefined sets of targets which can be then used in the initial, animate, and exit props.

They can be useful if you find yourself repeating the same animation multiple times. In this case, I knew that enter and exit animation would be the same, so I used it, but there are definitely better use cases for it.

"use client";

import { AnimatePresence, motion } from "motion/react";

const variants = {
  initial: { opacity: 0, y: -25 },
  visible: { opacity: 1, y: 0 },
  exit: { opacity: 0, y: 25 },
};

export function AnimatedState({ children }: { children: React.ReactNode }) {
  return (
    <AnimatePresence mode="popLayout" initial={false}>
      <motion.div
        initial="initial"
        animate="visible"
        exit="exit"
        variants={variants}
        transition={{ type: "spring", duration: 0.3, bounce: 0 }}
      >
        {children}
      </motion.div>
    </AnimatePresence>
  );
}

"use client";

import { AnimatePresence, motion } from "motion/react";

const variants = {
  initial: { opacity: 0, y: -25 },
  visible: { opacity: 1, y: 0 },
  exit: { opacity: 0, y: 25 },
};

export function AnimatedState({ children }: { children: React.ReactNode }) {
  return (
    <AnimatePresence mode="popLayout" initial={false}>
      <motion.div
        initial="initial"
        animate="visible"
        exit="exit"
        variants={variants}
        transition={{ type: "spring", duration: 0.3, bounce: 0 }}
      >
        {children}
      </motion.div>
    </AnimatePresence>
  );
}


id: framer-motion/the-basics/animating-height
lesson_id: framer-motion/the-basics
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: the-basics
lesson_title: The Basics
heading: Animating height
url_path: /lessons/framer-motion/the-basics/#animating-height
markdown_path: /md/sections/framer-motion/the-basics/animating-height.md
excerpt:
Framer Motion allows us to animate height from a fixed value to auto. The problem we are having here is that we want to animate from auto to auto, which Framer Motion doesn’t support. But often times we need to animate dynamic heights, like the Family drawer below. We will build the full version of the drawer in the walkthroughs later on by the way!

body:
Framer Motion allows us to animate height from a fixed value to auto. The problem we are having here is that we want to animate from auto to auto, which Framer Motion doesn’t support. But often times we need to animate dynamic heights, like the Family drawer below. We will build the full version of the drawer in the walkthroughs later on by the way!

How are we supposed to handle dynamic heights? Below is a simple example where clicking the button changes the content, which results in height change. Try to animate the height automatically when the content changes.

This one is not easy, it might be tricky, and perhaps frustrating. However, struggling and failing is much more productive than reading through the solution. When I started to learn how to code I didn’t want to struggle and I regret it. There is a small hint in the code below though!

import { motion } from "motion/react";
import { useState, useRef, useEffect } from "react";
import useMeasure from 'react-use-measure'

export default function Example() {
const [showExtraContent, setShowExtraContent] = useState(false);

return (
  <div className="wrapper">
    <button className="button" onClick={() => setShowExtraContent((b) => !b)}>
      Toggle height
    </button>
    <div className="element">
      <div className="inner">
        <h1>Fake Family Drawer</h1>
        <p>
          This is a fake family drawer. Animating height is tricky, but
          satisfying when it works.
        </p>
        {showExtraContent ? (
          <p>This extra content will change the height of the drawer. Some even more content to make the drawer taller and taller and taller...</p>
        ) : null}
      </div>
    </div>
  </div>
);
}


id: framer-motion/the-basics/when-should-you-use-framer-motion
lesson_id: framer-motion/the-basics
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: the-basics
lesson_title: The Basics
heading: When should you use Framer Motion?
url_path: /lessons/framer-motion/the-basics/#when-should-you-use-framer-motion
markdown_path: /md/sections/framer-motion/the-basics/when-should-you-use-framer-motion.md
excerpt:
Before we dive into more advanced concepts of Framer Motion, let’s talk about when you should use it.

body:
Before we dive into more advanced concepts of Framer Motion, let’s talk about when you should use it.

I tend to avoid using this library if I can achieve the same effect with CSS in a reasonable amount of time. Basically, I animate all enter and exit animations of UI elements like modals, dropdowns etc. through Radix, as it allows me to animate the exit with CSS.

@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes fadeOut {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

.dialog[data-state="open"] {
  animation: fadeIn 200ms ease-out;
}

.DialogContent[data-state="closed"] {
  animation: fadeOut 150ms ease-out;
}

@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes fadeOut {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

.dialog[data-state="open"] {
  animation: fadeIn 200ms ease-out;
}

.DialogContent[data-state="closed"] {
  animation: fadeOut 150ms ease-out;
}

Radix listens for animationstart event, and if an animation started when the open state changed to false they suspend the unmount while the animation plays out.

Whether to use Framer Motion or not also depends on how sensitive your app is to bundle size. At Vercel, we avoided using Framer Motion in Next.js’ docs, because we wanted to keep the bundle size as small as possible.

The copy animation below was initially using Framer Motion, as it was easier to build and maintain. But, we ended up switching to CSS animations to not use this library and reduce the bundle size.

But at Clerk, we are already using Framer Motion in our dashboard, so I reach for it more easily there to save time and make the code easier to read and maintain.

An animation in Clerk's dashboard that uses Framer Motion.

Framer Motion also has a guide on how to reduce bundle size which is worth checking out if you are concerned about it.


id: framer-motion/trash-interaction/installation
lesson_id: framer-motion/trash-interaction
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: trash-interaction
lesson_title: Trash interaction
heading: Installation
url_path: /lessons/framer-motion/trash-interaction/#installation
markdown_path: /md/sections/framer-motion/trash-interaction/installation.md
excerpt:
To get the codebase running locally, you first need to download it here. We switched from a GitHub repo, because it would be quite problematic to keep it private.

body:
To get the codebase running locally, you first need to download it here. We switched from a GitHub repo, because it would be quite problematic to keep it private.

After you have it on your device, you can run pnpm install and pnpm dev after.

The final code can be downloaded here.


id: framer-motion/trash-interaction/toolbar-animation
lesson_id: framer-motion/trash-interaction
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: trash-interaction
lesson_title: Trash interaction
heading: Toolbar animation
url_path: /lessons/framer-motion/trash-interaction/#toolbar-animation
markdown_path: /md/sections/framer-motion/trash-interaction/toolbar-animation.md
excerpt:
The toolbar appears once we select at least one image, and it disappears when we deselect all images. Because we need to hide it, we should wrap our toolbar in AnimatePresence and add an appropriate animation to it.

body:
The toolbar appears once we select at least one image, and it disappears when we deselect all images. Because we need to hide it, we should wrap our toolbar in AnimatePresence and add an appropriate animation to it.

<AnimatePresence>
  {imagesToRemove.length > 0 && !readyToRemove ? (
    <motion.div
      key="toolbar"
      initial={{ y: 20, opacity: 0, filter: "blur(4px)" }}
      animate={{
        y: 0,
        opacity: 1,
        filter: "blur(0px)",
      }}
      exit={{ y: 20, opacity: 0, filter: "blur(4px)" }}
      transition={{ duration: 0.3, bounce: 0, type: "spring" }}
      className="absolute bottom-8 flex gap-1 rounded-xl p-1 shadow-md"
    >
      <div className="flex w-full justify-between gap-1">
        <button className="flex w-12 flex-col items-center gap-px rounded-lg bg-gray-200 pb-1 pt-[6px] text-[10px] font-medium text-gray-900">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            className="size-4 shrink-0"
            viewBox="0 0 24 24"
            fill="none"
          >
            <path
              fillRule="evenodd"
              clipRule="evenodd"
              d="M10.8839 18.6339C10.3957 19.122 9.60427 19.122 9.11612 18.6339L3.36612 12.8839C3.1317 12.6495 3 12.3315 3 12C3 11.6685 3.13169 11.3506 3.36612 11.1161L9.11612 5.36612C9.60427 4.87796 10.3957 4.87796 10.8839 5.36612C11.372 5.85427 11.372 6.64573 10.8839 7.13388L7.26776 10.75H19.75C20.4404 10.75 21 11.3097 21 12C21 12.6904 20.4404 13.25 19.75 13.25H7.26777L10.8839 16.8661C11.372 17.3543 11.372 18.1457 10.8839 18.6339Z"
              fill="currentColor"
            />
          </svg>
          Back
        </button>
        <button
          onClick={() => {
            if (readyToRemove) {
              setRemove(true);
            } else {
              setReadyToRemove(true);
            }
          }}
          className="flex w-12 flex-col items-center gap-px rounded-lg bg-gray-200 pb-1 pt-[6px] text-[10px] font-medium text-gray-900 hover:bg-red-200 hover:text-red-900"
        >
          <svg
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 24 24"
            fill="none"
            className="size-4 shrink-0"
          >
            <path
              fillRule="evenodd"
              clipRule="evenodd"
              d="M7.58393 5C8.28068 3.24301 9.99487 2 12.0009 2C14.007 2 15.7212 3.24301 16.4179 5H21.25C21.6642 5 22 5.33579 22 5.75C22 6.16421 21.6642 6.5 21.25 6.5H19.9532L19.0588 20.3627C18.9994 21.2835 18.2352 22 17.3124 22H6.68756C5.76481 22 5.0006 21.2835 4.94119 20.3627L4.04683 6.5H2.75C2.33579 6.5 2 6.16421 2 5.75C2 5.33579 2.33579 5 2.75 5H7.58393ZM9.26161 5C9.83935 4.09775 10.8509 3.5 12.0009 3.5C13.151 3.5 14.1625 4.09775 14.7403 5H9.26161Z"
              fill="currentColor"
            />
          </svg>
          Trash
        </button>
        <button className="flex w-12 flex-col items-center gap-px rounded-lg bg-gray-200 pb-1 pt-[6px] text-[10px] font-medium text-gray-900">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            className="size-4"
            viewBox="0 0 24 24"
            fill="none"
          >
            <path
              fillRule="evenodd"
              clipRule="evenodd"
              d="M10.4902 2.84406C11.1661 1.69 12.8343 1.69 13.5103 2.84406L22.0156 17.3654C22.699 18.5321 21.8576 19.9999 20.5056 19.9999H3.49483C2.14281 19.9999 1.30147 18.5321 1.98479 17.3654L10.4902 2.84406ZM12 9C12.4142 9 12.75 9.33579 12.75 9.75V13.25C12.75 13.6642 12.4142 14 12 14C11.5858 14 11.25 13.6642 11.25 13.25V9.75C11.25 9.33579 11.5858 9 12 9ZM13 15.75C13 16.3023 12.5523 16.75 12 16.75C11.4477 16.75 11 16.3023 11 15.75C11 15.1977 11.4477 14.75 12 14.75C12.5523 14.75 13 15.1977 13 15.75Z"
              fill="currentColor"
            />
          </svg>
          Report
        </button>
      </div>
    </motion.div>
  ) : null}
</AnimatePresence>;

<AnimatePresence>
  {imagesToRemove.length > 0 && !readyToRemove ? (
    <motion.div
      key="toolbar"
      initial={{ y: 20, opacity: 0, filter: "blur(4px)" }}
      animate={{
        y: 0,
        opacity: 1,
        filter: "blur(0px)",
      }}
      exit={{ y: 20, opacity: 0, filter: "blur(4px)" }}
      transition={{ duration: 0.3, bounce: 0, type: "spring" }}
      className="absolute bottom-8 flex gap-1 rounded-xl p-1 shadow-md"
    >
      <div className="flex w-full justify-between gap-1">
        <button className="flex w-12 flex-col items-center gap-px rounded-lg bg-gray-200 pb-1 pt-[6px] text-[10px] font-medium text-gray-900">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            className="size-4 shrink-0"
            viewBox="0 0 24 24"
            fill="none"
          >
            <path
              fillRule="evenodd"
              clipRule="evenodd"
              d="M10.8839 18.6339C10.3957 19.122 9.60427 19.122 9.11612 18.6339L3.36612 12.8839C3.1317 12.6495 3 12.3315 3 12C3 11.6685 3.13169 11.3506 3.36612 11.1161L9.11612 5.36612C9.60427 4.87796 10.3957 4.87796 10.8839 5.36612C11.372 5.85427 11.372 6.64573 10.8839 7.13388L7.26776 10.75H19.75C20.4404 10.75 21 11.3097 21 12C21 12.6904 20.4404 13.25 19.75 13.25H7.26777L10.8839 16.8661C11.372 17.3543 11.372 18.1457 10.8839 18.6339Z"
              fill="currentColor"
            />
          </svg>
          Back
        </button>
        <button
          onClick={() => {
            if (readyToRemove) {
              setRemove(true);
            } else {
              setReadyToRemove(true);
            }
          }}
          className="flex w-12 flex-col items-center gap-px rounded-lg bg-gray-200 pb-1 pt-[6px] text-[10px] font-medium text-gray-900 hover:bg-red-200 hover:text-red-900"
        >
          <svg
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 24 24"
            fill="none"
            className="size-4 shrink-0"
          >
            <path
              fillRule="evenodd"
              clipRule="evenodd"
              d="M7.58393 5C8.28068 3.24301 9.99487 2 12.0009 2C14.007 2 15.7212 3.24301 16.4179 5H21.25C21.6642 5 22 5.33579 22 5.75C22 6.16421 21.6642 6.5 21.25 6.5H19.9532L19.0588 20.3627C18.9994 21.2835 18.2352 22 17.3124 22H6.68756C5.76481 22 5.0006 21.2835 4.94119 20.3627L4.04683 6.5H2.75C2.33579 6.5 2 6.16421 2 5.75C2 5.33579 2.33579 5 2.75 5H7.58393ZM9.26161 5C9.83935 4.09775 10.8509 3.5 12.0009 3.5C13.151 3.5 14.1625 4.09775 14.7403 5H9.26161Z"
              fill="currentColor"
            />
          </svg>
          Trash
        </button>
        <button className="flex w-12 flex-col items-center gap-px rounded-lg bg-gray-200 pb-1 pt-[6px] text-[10px] font-medium text-gray-900">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            className="size-4"
            viewBox="0 0 24 24"
            fill="none"
          >
            <path
              fillRule="evenodd"
              clipRule="evenodd"
              d="M10.4902 2.84406C11.1661 1.69 12.8343 1.69 13.5103 2.84406L22.0156 17.3654C22.699 18.5321 21.8576 19.9999 20.5056 19.9999H3.49483C2.14281 19.9999 1.30147 18.5321 1.98479 17.3654L10.4902 2.84406ZM12 9C12.4142 9 12.75 9.33579 12.75 9.75V13.25C12.75 13.6642 12.4142 14 12 14C11.5858 14 11.25 13.6642 11.25 13.25V9.75C11.25 9.33579 11.5858 9 12 9ZM13 15.75C13 16.3023 12.5523 16.75 12 16.75C11.4477 16.75 11 16.3023 11 15.75C11 15.1977 11.4477 14.75 12 14.75C12.5523 14.75 13 15.1977 13 15.75Z"
              fill="currentColor"
            />
          </svg>
          Report
        </button>
      </div>
    </motion.div>
  ) : null}
</AnimatePresence>;


id: framer-motion/trash-interaction/transition-into-the-trash
lesson_id: framer-motion/trash-interaction
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: trash-interaction
lesson_title: Trash interaction
heading: Transition into the trash
url_path: /lessons/framer-motion/trash-interaction/#transition-into-the-trash
markdown_path: /md/sections/framer-motion/trash-interaction/transition-into-the-trash.md
excerpt:
We’ll need to use the layoutId prop here. That way we tell Framer Motion that the images in the grid should become the images in the trash. Let’s add the prop to both the grid and the trash images.

body:
We’ll need to use the layoutId prop here. That way we tell Framer Motion that the images in the grid should become the images in the trash. Let’s add the prop to both the grid and the trash images.

<motion.img
  layoutId={`image-${image}`}
  src={`/how-i-use-framer-motion/why-framer-motion/${image}.webp`}
  height={65}
  width={65}
  style={{
    borderRadius: 6,
    rotate:
      index % 2 === 0
        ? 4 * (imagesToRemove.length - index + 1)
        : -1 * (imagesToRemove.length - index + 1) * 4,
  }}
/>;

<motion.img
  layoutId={`image-${image}`}
  src={`/how-i-use-framer-motion/why-framer-motion/${image}.webp`}
  height={65}
  width={65}
  style={{
    borderRadius: 6,
    rotate:
      index % 2 === 0
        ? 4 * (imagesToRemove.length - index + 1)
        : -1 * (imagesToRemove.length - index + 1) * 4,
  }}
/>;


id: framer-motion/trash-interaction/trash-enter-and-exit-animation
lesson_id: framer-motion/trash-interaction
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: trash-interaction
lesson_title: Trash interaction
heading: Trash enter and exit animation
excerpt: We’ll wrap our trash with AnimatePresence and add a small animation to it.
url_path: /lessons/framer-motion/trash-interaction/#trash-enter-and-exit-animation
markdown_path: /md/sections/framer-motion/trash-interaction/trash-enter-and-exit-animation.md
body:
We’ll wrap our trash with AnimatePresence and add a small animation to it.

<motion.div
  initial={{ opacity: 0, filter: "blur(4px)", scale: 1.2 }}
  animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
  exit={{ opacity: 0, filter: "blur(4px)", scale: 1.2 }}
  transition={{ type: "spring", duration: 0.2, bounce: 0 }}
>
  <TrashBack />
</motion.div>;

<motion.div
  initial={{ opacity: 0, filter: "blur(4px)", scale: 1.2 }}
  animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
  exit={{ opacity: 0, filter: "blur(4px)", scale: 1.2 }}
  transition={{ type: "spring", duration: 0.2, bounce: 0 }}
>
  <TrashBack />
</motion.div>;


id: framer-motion/trash-interaction/instant-removal-fix
lesson_id: framer-motion/trash-interaction
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: trash-interaction
lesson_title: Trash interaction
heading: Instant removal fix
url_path: /lessons/framer-motion/trash-interaction/#instant-removal-fix
markdown_path: /md/sections/framer-motion/trash-interaction/instant-removal-fix.md
excerpt:
If we don’t select all images, the unselected ones disappear instantly. To fix that, we can wrap our images grid in AnimatePresence and add an exit animation to it (the addition of an exit animation is not included in the video).

body:
If we don’t select all images, the unselected ones disappear instantly. To fix that, we can wrap our images grid in AnimatePresence and add an exit animation to it (the addition of an exit animation is not included in the video).

<motion.li
  key={image}
  className="relative flex"
  exit={
    !isSelected
      ? {
          opacity: 0,
          filter: "blur(4px)",
          transition: { duration: 0.05 },
        }
      : {}
  }
>

<motion.li
  key={image}
  className="relative flex"
  exit={
    !isSelected
      ? {
          opacity: 0,
          filter: "blur(4px)",
          transition: { duration: 0.05 },
        }
      : {}
  }
>


id: framer-motion/trash-interaction/images-are-going-through-the-trash
lesson_id: framer-motion/trash-interaction
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: trash-interaction
lesson_title: Trash interaction
heading: Images are going through the trash
url_path: /lessons/framer-motion/trash-interaction/#images-are-going-through-the-trash
markdown_path: /md/sections/framer-motion/trash-interaction/images-are-going-through-the-trash.md
excerpt:
To fix the issue with images going through the trash, we can ensure that our <TrashFront /> component becomes visible after our images are almost done animating. We need to time it essentially.

body:
To fix the issue with images going through the trash, we can ensure that our <TrashFront /> component becomes visible after our images are almost done animating. We need to time it essentially.

<motion.div
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
  exit={{ opacity: 0 }}
  transition={{
    duration: 0,
    delay: 0.175,
  }}
  className="absolute bottom-0 left-[3px] h-full w-[90px]"
>
  <TrashFront />
</motion.div>;

<motion.div
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
  exit={{ opacity: 0 }}
  transition={{
    duration: 0,
    delay: 0.175,
  }}
  className="absolute bottom-0 left-[3px] h-full w-[90px]"
>
  <TrashFront />
</motion.div>;


id: framer-motion/trash-interaction/motion-config
lesson_id: framer-motion/trash-interaction
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: trash-interaction
lesson_title: Trash interaction
heading: Motion Config
url_path: /lessons/framer-motion/trash-interaction/#motion-config
markdown_path: /md/sections/framer-motion/trash-interaction/motion-config.md
excerpt:
I’m not the biggest fan of the transition right now, but I don’t want to adjust it for each item separately. This is a perfect use case for MotionConfig.

body:
I’m not the biggest fan of the transition right now, but I don’t want to adjust it for each item separately. This is a perfect use case for MotionConfig.

<MotionConfig transition={{ type: "spring", duration: 0.5, bounce: 0.2 }}>
  // ...
</MotionConfig>

<MotionConfig transition={{ type: "spring", duration: 0.5, bounce: 0.2 }}>
  // ...
</MotionConfig>


id: framer-motion/trash-interaction/dropping-into-the-bin
lesson_id: framer-motion/trash-interaction
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: trash-interaction
lesson_title: Trash interaction
heading: Dropping into the bin
url_path: /lessons/framer-motion/trash-interaction/#dropping-into-the-bin
markdown_path: /md/sections/framer-motion/trash-interaction/dropping-into-the-bin.md
excerpt:
We now need to create this nice drop animation. However, we can’t influence the way shared layout animations are done. What we can do is we can animate the parent div so that the images will follow it.

body:
We now need to create this nice drop animation. However, we can’t influence the way shared layout animations are done. What we can do is we can animate the parent div so that the images will follow it.

<motion.div
  animate={{
    y: 73,
  }}
  transition={{ delay: 0.13 }}
  className="absolute flex w-full top-[-60px] flex-col-reverse items-center"
>
 // ...
</motion.div>;

<motion.div
  animate={{
    y: 73,
  }}
  transition={{ delay: 0.13 }}
  className="absolute flex w-full top-[-60px] flex-col-reverse items-center"
>
 // ...
</motion.div>;


id: framer-motion/trash-interaction/key-takeaways
lesson_id: framer-motion/trash-interaction
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: trash-interaction
lesson_title: Trash interaction
heading: Key takeaways
url_path: /lessons/framer-motion/trash-interaction/#key-takeaways
markdown_path: /md/sections/framer-motion/trash-interaction/key-takeaways.md
body:
We can animate the parent of our shared layout animation to influence how the children are animated. MotionConfig is a great way to set a default transition for all animations in a component.


id: framer-motion/why-framer-motion/lost-in-the-mountains
lesson_id: framer-motion/why-framer-motion
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: why-framer-motion
lesson_title: Why Framer Motion
heading: Lost in the mountains
excerpt: Be careful.
url_path: /lessons/framer-motion/why-framer-motion/#lost-in-the-mountains
markdown_path: /md/sections/framer-motion/why-framer-motion/lost-in-the-mountains.md
body:
Be careful.

The documentation often times follows a happy path. It covers simple animations, which are great for beginners, but don’t give you a lot of insight on how to craft more complex ones.

That’s why, in this part, we’ll build a lot. We’ll cover the basics, but we’ll quickly transition into building more complex animations like the Feedback popover below. We’ll run into issues and solve them, so that you know what to do when you run into similar problems yourself.

Before we start coding, let’s see why Framer Motion is so powerful and what makes it stand out from other libraries.


id: framer-motion/why-framer-motion/the-power-of-framer-motion
lesson_id: framer-motion/why-framer-motion
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: why-framer-motion
lesson_title: Why Framer Motion
heading: The power of Framer Motion
url_path: /lessons/framer-motion/why-framer-motion/#the-power-of-framer-motion
markdown_path: /md/sections/framer-motion/why-framer-motion/the-power-of-framer-motion.md
excerpt:
Everything that is possible with Framer Motion is possible with vanilla CSS and JS as well. It just usually takes a lot more time.

body:
Everything that is possible with Framer Motion is possible with vanilla CSS and JS as well. It just usually takes a lot more time.

How would you code the animation below without this library? You’d need to calculate the highlight’s new position based on the tab’s dimensions and its distance from the left side. That’s only the highlight part. Now, we’d need to find a way to smoothly change the direction (button in the bottom right corner) as well.

Saved Sites Collections 48 Following 32 Followers

This whole component, including the styles and inline SVGs, is only 76 lines of code. That alone would convince me to start using Framer Motion for more complex animations. You can view my implementation of this animation here.

Remember the example below when we discussed spring animations? It consists of 33 lines of code, is interruptible and maintains momentum. Now, try building it again without Framer Motion or a similar library. It’d be a lot of work.

Also, in React, it’s usually difficult to animate components once they’ve been removed from the DOM, because, well... they’re not there anymore. By wrapping your components with AnimatePresence provided by Framer Motion, you can animate them out as well.

import { motion, AnimatePresence } from "motion/react";

export function Component({ isVisible }: { isVisible: boolean }) {
  return (
    <AnimatePresence>
      {isVisible ? (
        <motion.div
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
        />
      ) : null}
    </AnimatePresence>
  );
}

import { motion, AnimatePresence } from "motion/react";

export function Component({ isVisible }: { isVisible: boolean }) {
  return (
    <AnimatePresence>
      {isVisible ? (
        <motion.div
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
        />
      ) : null}
    </AnimatePresence>
  );
}

Additionally, thanks to layout animations, which we will cover in the next lesson, we are able to easily create interactions like the one below.

You can also create drag-and-drop interactions, draw svgs, and more.

Framer Motion is not the only animation library out there though. GSAP and react-spring are two popular alternatives. Let’s see how they compare.


id: framer-motion/why-framer-motion/react-spring
lesson_id: framer-motion/why-framer-motion
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: why-framer-motion
lesson_title: Why Framer Motion
heading: React Spring
url_path: /lessons/framer-motion/why-framer-motion/#react-spring
markdown_path: /md/sections/framer-motion/why-framer-motion/react-spring.md
excerpt:
A spring-based library, powerful, and highly configurable. It has a steep learning curve, but at the same time it gives you a lot of control.

body:
A spring-based library, powerful, and highly configurable. It has a steep learning curve, but at the same time it gives you a lot of control.

Below you can see how you can create a simple fade-in animation with React Spring.

import { useSpring, animated } from "@react-spring/web";

function MyComponent() {
  const [props, api] = useSpring(
    () => ({
      from: { opacity: 0 },
      to: { opacity: 1 },
    }),
    [],
  );

  return <animated.div style={props}>Hello World</animated.div>;
}

import { useSpring, animated } from "@react-spring/web";

function MyComponent() {
  const [props, api] = useSpring(
    () => ({
      from: { opacity: 0 },
      to: { opacity: 1 },
    }),
    [],
  );

  return <animated.div style={props}>Hello World</animated.div>;
}

It’s made by Poimandres, and because of that, this library works very well with other Poimandres libraries like use-gesture for example.

The code of this example is available here.

Pros:

Spring-based animations. Smaller package size than Framer Motion. Highly-configurable.

Cons:

Steep learning curve. It takes more time and more code to write the same animations as in Framer Motion. Documentation is hard to parse at times.


id: framer-motion/why-framer-motion/gsap
lesson_id: framer-motion/why-framer-motion
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: why-framer-motion
lesson_title: Why Framer Motion
heading: GSAP
url_path: /lessons/framer-motion/why-framer-motion/#gsap
markdown_path: /md/sections/framer-motion/why-framer-motion/gsap.md
excerpt:
The first animation library I’ve ever used. We’ve talked about websites on awwwwards in the lesson about taste, GSAP powers a lot of these. It’s framework-agnostic and doesn’t support spring animations. I see framework-agnostic as a con, because framework-specific libraries usually provide a better developer experience. Though GSAP has a React specific hook which you can see below.

body:
The first animation library I’ve ever used. We’ve talked about websites on awwwwards in the lesson about taste, GSAP powers a lot of these. It’s framework-agnostic and doesn’t support spring animations. I see framework-agnostic as a con, because framework-specific libraries usually provide a better developer experience. Though GSAP has a React specific hook which you can see below.

import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";

gsap.registerPlugin(useGSAP);

const container = useRef();

useGSAP(
  () => {
    gsap.to(".box", { opacity: 1 });
  },
  { scope: container },
);

import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";

gsap.registerPlugin(useGSAP);

const container = useRef();

useGSAP(
  () => {
    gsap.to(".box", { opacity: 1 });
  },
  { scope: container },
);

It’s open-source, but you can’t use it in commercial projects on the free plan. Additionally, it has a rich plugin system, but some plugins are only available on the paid plan. You can see the pricing here.


id: framer-motion/why-framer-motion/update-to-gsap-s-pricing
lesson_id: framer-motion/why-framer-motion
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: why-framer-motion
lesson_title: Why Framer Motion
heading: Update to GSAP’s pricing
url_path: /lessons/framer-motion/why-framer-motion/#update-to-gsap-s-pricing
markdown_path: /md/sections/framer-motion/why-framer-motion/update-to-gsap-s-pricing.md
excerpt:
Since this lesson was published, GSAP has been acquired by Webflow and all of its features are now free.

body:
Since this lesson was published, GSAP has been acquired by Webflow and all of its features are now free.

This is made with GSAP.

Pros:

A very useful timeline feature. Easier to learn than the alternatives (in my opinion). Framework-agnostic can be seen as a pro to some. Large community and good documentation.

Cons:

Some plugins require a paid plan. No spring animations. Not tailored to React. Paid plan for commercial projects.


id: framer-motion/why-framer-motion/framer-motion
lesson_id: framer-motion/why-framer-motion
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: why-framer-motion
lesson_title: Why Framer Motion
heading: Framer Motion
url_path: /lessons/framer-motion/why-framer-motion/#framer-motion
markdown_path: /md/sections/framer-motion/why-framer-motion/framer-motion.md
excerpt:
You can write declarative and imperative animations with it. Declarative ones can be defined as simple descriptions. This API avoids the need for you to interact with the DOM via selectors, refs or other kinds of error-prone wiring. There’s less room for error and it’s easier to scale.

body:
You can write declarative and imperative animations with it. Declarative ones can be defined as simple descriptions. This API avoids the need for you to interact with the DOM via selectors, refs or other kinds of error-prone wiring. There’s less room for error and it’s easier to scale.

<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />

<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />

Imperative animations are more low-level, more powerful, but harder to write and maintain. You can read more about the differences here. I personally almost never use imperative animations myself.

function ImperativeComponent() {
  const [scope, animate] = useAnimate();

  useEffect(() => {
    // This "li" selector will only select children
    // of the element that receives `scope`.
    animate("li", { opacity: 1 });
  });

  return <ul ref={scope}>{children}</ul>;
}

function ImperativeComponent() {
  const [scope, animate] = useAnimate();

  useEffect(() => {
    // This "li" selector will only select children
    // of the element that receives `scope`.
    animate("li", { opacity: 1 });
  });

  return <ul ref={scope}>{children}</ul>;
}

Thanks to layout animations Framer Motion can animate properties that are not possible to animate with CSS, like flex-direction. The direction change in the example from the beginning of this lesson is done by simply changing the flex-direction from row to column.

Saved Sites Collections 48 Following 32 Followers

By using shared layout animations we can connect two components and create a transition between them when the first one is removed. That’s how the trash interaction is done. We aren’t moving the images ourselves. The images in the trash are completely different elements than the selectable ones. By using the same layoutId we tell Framer Motion to animate them to the place where the images in the trash will be rendered.

My favorite example that showcases the power of Framer Motion is the one below. It feels incredibly native to me.

Because these animations are relatively easy to build, there’s a lot of "magic" that is happening behind the scenes. I often run into a situation where something is not working, and I can’t understand why, because I don’t know how Framer Motion works under the hood.

Docs often follow the happy path, which is great for beginners, but when you run into an issue that is not covered in the docs, it can be hard to find a solution.

Pros:

Spring animations. Layout animations are an absolute game-changer. Complex animations with not a lot of code. The API ties in well with React.

Cons:

Bundle size. Sometimes, it’s hard to understand why an animation is not working as expected.


id: framer-motion/why-framer-motion/framer-motion-is-now-motion
lesson_id: framer-motion/why-framer-motion
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: why-framer-motion
lesson_title: Why Framer Motion
heading: Framer Motion is now Motion
url_path: /lessons/framer-motion/why-framer-motion/#framer-motion-is-now-motion
markdown_path: /md/sections/framer-motion/why-framer-motion/framer-motion-is-now-motion.md
excerpt:
Framer Motion has been renamed to Motion for React. Nothing except the import path has changed. To import motion from Motion for React you need to do the following:

body:
Framer Motion has been renamed to Motion for React. Nothing except the import path has changed. To import motion from Motion for React you need to do the following:

import { motion } from "motion/react";

import { motion } from "motion/react";

Previously, you would’ve imported it like this:

import { motion } from "framer-motion";

import { motion } from "framer-motion";

I will keep referring to it as Framer Motion in this course to avoid confusion as mentioned in the CSS Module, but the imports in all exercises have been updated.

With that being said, Motion (not Motion for React) is a good alternative to Framer Motion when you’re not using React. You can read more about it here. If you are working with React, I would still recommend Framer Motion as it’s more tailored to the framework.


id: framer-motion/why-framer-motion/other-alternatives
lesson_id: framer-motion/why-framer-motion
module_slug: framer-motion
module_title: Framer Motion
lesson_slug: why-framer-motion
lesson_title: Why Framer Motion
heading: Other alternatives
url_path: /lessons/framer-motion/why-framer-motion/#other-alternatives
markdown_path: /md/sections/framer-motion/why-framer-motion/other-alternatives.md
excerpt:
We haven’t covered every library out there, only the most popular ones. There are many other libraries like Anime.js or Popmotion, but I haven’t tried any of those and basically stick to Framer Motion.

body:
We haven’t covered every library out there, only the most popular ones. There are many other libraries like Anime.js or Popmotion, but I haven’t tried any of those and basically stick to Framer Motion.

There’s also the Web Animation API, which is a native browser API for animations. It’s not as powerful as the libraries we’ve discussed, but it’s definitely something you can check out.

Let’s start building some animations now!


id: good-vs-great-animations/accessibility/overview
lesson_id: good-vs-great-animations/accessibility
module_slug: good-vs-great-animations
module_title: Good vs Great Animations
lesson_slug: accessibility
lesson_title: Accessibility
heading: Overview
url_path: /lessons/good-vs-great-animations/accessibility/#overview
markdown_path: /md/sections/good-vs-great-animations/accessibility/overview.md
excerpt:
The interactive examples and code snippets in this article are crucial to understanding the concepts like in the last lesson, so this one is text-based as well, but there is an exercise with a video for the solution.

body:
The interactive examples and code snippets in this article are crucial to understanding the concepts like in the last lesson, so this one is text-based as well, but there is an exercise with a video for the solution.

Animations are used to strategically improve an experience. To some people, animations actually degrade the experience. Animations can make people feel sick or get distracted. That’s not the experience we want to build. To prevent degrading the experience, our animations need to account for people who don’t want animations.

Most devices today allow users to convey their preference for animations.

You can read this preference in browsers using the prefers-reduced-motion CSS media query.

prefers-reduced-motion: no-preference means the user has not set a preference. No changes to your animation needed. prefers-reduced-motion: reduce means the user has set a preference. Your animation should be altered.

When that preference is present, we should consider removing, reducing or replacing animations.


