Adding fancy animations

If you're reading this from pc, try to hover over some buttons in my navbar. Fancy, right? This is done with Framer Motion.

Framer motion is react animation library. In the rather old update they added layout animations. This is badly documented, as i couldn't find information about layout animations that i needed quickly.

So, how one does this?

What are layout animations

Firstly, we need to understand what layout animations are.
So, what is a layout animation, you may ask. This is rather simple. When you animate any of the following, this is considered layout animation:

  1. Position CSS (flex, grid, etc)
  2. Size css (width, height, padding and so on)
  3. Position relative to another elements (i. e. list of posts reordering)

Adding a hover animation

Now that we understand what is layout animation, the only thing left to do implement this.
First of all, you should have a focused element state. In react you would do it like this before your return:

const [focused, setFocused] = useState(null)

You can use any state manager you want. I went with default react one, as it's simple and we don't need anything else for this example.
Now, say you have a <div> with some <motion.div>s from framer-motion. This may look like this:

<div>
  <Link href="#" onFocus={() => setFocused("firstlink")}>
    <Button variant="ghost" className="relative ...">
      <p className={cn("z-20")}>Link 1 title</p>
      {focused === "firstlink" && (
        <motion.div className="absolute ..." layoutId="main_nav_highlight" />
      )}
    </Button>
  </Link>
  <Link href="#" onFocus={() => setFocused("secondlink")}>
    <Button variant="ghost" className="relative ...">
      <p className={cn("z-20")}>Link 2 title</p>
      {focused === "secondlink" && (
        <motion.div
          className="absolute w-full h-full ..."
          layoutId="main_nav_highlight"
        />
      )}
    </Button>
  </Link>
</div>

Here we have a few things to note. I use onFocus on <Link> to set focus. I recommend doing the same if you're using Next.js like me. On the <Button> i have a relative class, so that <motion.div> will take up the entire button.
We show <motion.div> only when the element is focused. You don't need <AnimatePresence> here, as this is a layout animation.

Adding fading

The example above works, but it lacks fade-in and fade-out animations on hover. To implement them, let's change the code a little.

Change your motion.div part into something like this

<AnimatePresence>
  {focused === "linkid" && (
    <motion.div
      transition={{
        layout: {
          duration: 0.2,
          ease: "easeOut",
        },
        duration: 0.2,
      }}
      key={`linknum_motiondiv`}
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
      className="absolute h-full w-full ..."
      layoutId="main_nav_highlight"
    />
  )}
</AnimatePresence>

Here we add AnimatePresence. Without it, the exit animation won't work as the motion.div will just be deleted.
exit, animate and initial are just simple fade-in and fade-out animations.
The final code should look like this:

<div>
  <Link href="#" onFocus={() => setFocused("firstlink")}>
    <Button variant="ghost" className="relative ...">
      <p className={cn("z-20")}>Link 1 title</p>
      <AnimatePresence>
        {focused === "firstlink" && (
          <motion.div
            className="absolute ..."
            layoutId="main_nav_highlight"
            transition={{
              layout: {
                duration: 0.2,
                ease: "easeOut",
              },
              duration: 0.2,
            }}
            key={`linknum_motiondiv`}
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
          />
        )}
      </AnimatePresence>
    </Button>
  </Link>
  <Link href="#" onFocus={() => setFocused("secondlink")}>
    <Button variant="ghost" className="relative ...">
      <p className={cn("z-20")}>Link 2 title</p>
 
      <AnimatePresence>
        {focused === "secondlink" && (
          <motion.div
            transition={{
              layout: {
                duration: 0.2,
                ease: "easeOut",
              },
              duration: 0.2,
            }}
            key={`linknum_motiondiv`}
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            className="absolute w-full h-full ..."
            layoutId="main_nav_highlight"
          />
        )}
      </AnimatePresence>
    </Button>
  </Link>
</div>

Bonus: Adding page transitions

Now you know how to do probably the most impressive part. Now all that's left is to add simple page transition animations.
I will add a simple fade-in and fade-out animations on page transition, as more complex animations require more code and deeper understanding of what framer-motion does.

Add a client component for it.
The component is simple, it's that you just need 'use client' to use pathname.

'use client'
 
import { motion } from 'framer-motion'
import { usePathname } from 'next/navigation'
 
export function AnimationProvider({ children }) {
  const pathname = usePathname()
  return (
    <motion.div
      key={pathname}
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    >
      {children}
    </motion.div>
  )
}

Then you just wrap your content in layout.tsx and you're basically done.

<main>
  <AnimationProvider>
    {children}
  </AnimationProvider>
</main>

That's it for today.