Search code examples
reactjsanimationnext.jsframer-motion

Animating Navigation using Framer Motion


I have an issue with the animation of my navigation bar. I am using React and Framer Motion. My result is almost there, but it still needs a little touch to make it pixel perfect. I am struggling with the animation of the links; they should be locked on the left, but mine slide in from the right. They should also reveal from left to right, one by one. Could you please check my code and suggest some improvements to achieve the desired result? Here is a short video of how it should animate: https://www.loom.com/share/d882818c384e4af18abeabf92abb2980


import Link from "next/link";
import React, { useState } from "react";
import HamburgerIcon from "./icons/HamburgerIcon";
import { motion } from "framer-motion";

const MenuDesktop = () => {
  const [isHovered, setIsHovered] = useState(false);

  const navVariants = {
    initial: { width: "5rem", opacity: 0, zIndex: 0 },
    hover: { width: "100%", opacity: 1, zIndex: 1 },
  };

  const linkVariants = {
    initial: { opacity: 0 },
    hover: { opacity: 1 },
  };

  const iconVariants = {
    initial: { opacity: 1 },
    hover: { opacity: 0 },
  };

  return (
    <div className="flex p-0.5 h-20 rounded-15px bg-cc-invert justify-center items-stretch overflow-hidden">
      <div className="relative">
        <motion.nav
          className="flex relative h-full overflow-auto"
          initial="initial"
          animate={isHovered ? "hover" : "initial"}
          variants={navVariants}
          transition={{
            width: { duration: 0.75, ease: "easeInOut" },
            opacity: { duration: 1, ease: "easeInOut" },
          }}
          onHoverStart={() => setIsHovered(true)}
          onHoverEnd={() => setIsHovered(false)}
        >
          <motion.div
            className="font-dmMono tracking-wider text-cc-dark-brown flex items-center gap-2 text-xs uppercase pl-4 bg-cc-invert "
            initial="initial"
            animate={isHovered ? "hover" : "initial"}
            variants={linkVariants}
            transition={{ duration: 0.5 }}
          >
            <Link className="px-2 whitespace-nowrap" href="/development">
              Development
            </Link>
            <Link className="px-2 whitespace-nowrap" href="/energo">
              Energo
            </Link>
            <Link className="px-2 whitespace-nowrap" href="/company">
              Company
            </Link>
            <Link className="px-2 whitespace-nowrap" href="/news">
              News
            </Link>
            <Link className="px-2 whitespace-nowrap" href="/career">
              Career
            </Link>
          </motion.div>
        </motion.nav>
        <motion.div
          className="absolute top-7 right-7 flex items-center cursor-pointer z-0"
          initial="initial"
          animate={isHovered ? "hover" : "initial"}
          variants={iconVariants}
          transition={{ duration: 0.5 }}
        >
          <HamburgerIcon className="h-5 cursor-pointer" />
        </motion.div>
      </div>
      <Link
        href="/"
        className="font-dmMono tracking-wider text-cc-dark-brown flex items-center justify-center p-4 bg-cc-light-brown rounded-15px"
      >
        <span className="uppercase text-xs px-2">Contact</span>
      </Link>
    </div>
  );
};

export default MenuDesktop;

Solution

  • The animation you're looking for can be achieved by using the staggerChildren property in Framer Motion. This property allows you to delay the animation of direct children.

    import Link from "next/link";
    import React, {
      useState
    } from "react";
    import HamburgerIcon from "./icons/HamburgerIcon";
    import {
      motion
    } from "framer-motion";
    
    const MenuDesktop = () => {
      const [isHovered, setIsHovered] = useState(false);
    
      const navVariants = {
        initial: {
          width: "5rem",
          opacity: 0,
          zIndex: 0
        },
        hover: {
          width: "100%",
          opacity: 1,
          zIndex: 1
        },
      };
    
      const linkContainerVariants = {
        initial: {
          opacity: 0
        },
        hover: {
          opacity: 1,
          transition: {
            staggerChildren: 0.1,
            delayChildren: 0.2
          }
        },
      };
    
      const linkVariants = {
        initial: {
          x: -50,
          opacity: 0
        },
        hover: {
          x: 0,
          opacity: 1
        },
      };
    
      const iconVariants = {
        initial: {
          opacity: 1
        },
        hover: {
          opacity: 0
        },
      };
    
      return ( <
        div className = "flex p-0.5 h-20 rounded-15px bg-cc-invert justify-center items-stretch overflow-hidden" >
        <
        div className = "relative" >
        <
        motion.nav className = "flex relative h-full overflow-auto"
        initial = "initial"
        animate = {
          isHovered ? "hover" : "initial"
        }
        variants = {
          navVariants
        }
        transition = {
          {
            width: {
              duration: 0.75,
              ease: "easeInOut"
            },
            opacity: {
              duration: 1,
              ease: "easeInOut"
            },
          }
        }
        onHoverStart = {
          () => setIsHovered(true)
        }
        onHoverEnd = {
          () => setIsHovered(false)
        } >
        <
        motion.div className = "font-dmMono tracking-wider text-cc-dark-brown flex items-center gap-2 text-xs uppercase pl-4 bg-cc-invert "
        initial = "initial"
        animate = {
          isHovered ? "hover" : "initial"
        }
        variants = {
          linkContainerVariants
        } >
        {
          ["Development", "Energo", "Company", "News", "Career"].map((link) => ( <
            motion.div variants = {
              linkVariants
            }
            transition = {
              {
                duration: 0.5
              }
            } >
            <
            Link className = "px-2 whitespace-nowrap"
            href = {
              `/${link.toLowerCase()}`
            } > {
              link
            } <
            /Link> <
            /motion.div>
          ))
        } <
        /motion.div> <
        /motion.nav> <
        motion.div className = "absolute top-7 right-7 flex items-center cursor-pointer z-0"
        initial = "initial"
        animate = {
          isHovered ? "hover" : "initial"
        }
        variants = {
          iconVariants
        }
        transition = {
          {
            duration: 0.5
          }
        } >
        <
        HamburgerIcon className = "h-5 cursor-pointer" / >
        <
        /motion.div> <
        /div> <
        Link href = "/"
        className = "font-dmMono tracking-wider text-cc-dark-brown flex items-center justify-center p-4 bg-cc-light-brown rounded-15px" >
        <
        span className = "uppercase text-xs px-2" > Contact < /span> <
        /Link> <
        /div>
      );
    };
    
    export default MenuDesktop;