Creating an animated navbar inspired by Vercel using React (Next.js v13), Framer-Motion, and Tailwind CSS

ashish - Aug 9 '23 - - Dev Community

While building web apps, I usually like to take inspiration from other sites. I'm always fascinated by beautifully designed websites with subtle yet cool animations. Vercel is one of those sites I really like as a web developer. Also, Vercel's design & frontend team is one of the best out there. So, a few days back while I was deploying one of my apps on Vercel, I noticed there navbar component had a subtle hover animation which felt really smooth. Basically, what was happening was that each navbar link / tab had a background color which would follow the cursor on hover over the navbar. Since, I'm currently building the next version of my personal site (using Next.js v13), I decided to implement it in my site as well. You can think of this article as a guide to creating the navbar yourself! Here's what the navbar will look and work like -

First Steps

I already mentioned earlier that the site is being built using Next.js v13. So, the first thing you would need to do is scaffold a next app using this command. While doing this, you will get prompted about whether you want to add Tailwind to the project, make you're you add it and also make sure you use the app directory and /src folder so that we are on the same page while working -

nextjs cli



pnpm create next-app@latest


Enter fullscreen mode Exit fullscreen mode

The next thing would be to install the dependencies required, mainly Framer-Motion in this case -



pnpm i framer-motion


Enter fullscreen mode Exit fullscreen mode

Start the dev server -



pnpm dev


Enter fullscreen mode Exit fullscreen mode

Now that we have our basic project ready, we can start building the navbar!

Building the Navbar component

Let's create our basic styled navbar component first and it to our global layout file in the project. If you're not familiar with what a layout file is - it's basically a new file type introduced in Next.js v13 which can be used to create the layout for your site. By default, Next.js would create a layout file for you in the root of your app named layout.tsx.

Create a /components folder inside your /app directory and create a file named Navbar.tsx inside it.



// src/app/components/Navbar.tsx

"use client";

import { usePathname } from "next/navigation";
import Link from "next/link";

const navItems = [
  {
    path: "/",
    name: "Home",
  },
  {
    path: "/now",
    name: "Now",
  },
  {
    path: "/guestbook",
    name: "Guestbook",
  },
  {
    path: "/writing",
    name: "Writing",
  },
];

export default function NavBar() {
  let pathname = usePathname() || "/";
  
  return (
    <div className="border border-stone-800/90 p-[0.4rem] rounded-lg mb-12 sticky top-4 z-[100] bg-stone-900/80 backdrop-blur-md">
      <nav className="flex gap-2 relative justify-start w-full z-[100]  rounded-lg">
        {navItems.map((item, index) => {
          const isActive = item.path === pathname;

          return (
            <Link
              key={item.path}
              className={`px-4 py-2 rounded-md text-sm lg:text-base relative no-underline duration-300 ease-in ${
                isActive ? "text-zinc-100" : "text-zinc-400"
              }`}
              href={item.path}
            >
              <span>{item.name}</span>
            </Link>
          );
        })}
      </nav>
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

This is the basic navbar component we can have. If you don't understand what "use client" at the top of the file is, you need to read the Next.js docs and a little about React Server Components. In short, all react components in Next.js v13 are server components by default which means that they run on the server. So, if you want to use client based hooks like useState or useEffect , you need to make them client components using the line "use client".

Now let's break down the rest of the code. Firstly, I have defined an array of the items I want to have on my navbar. In my case they are, /, /now, /guestbook, & /writing . The next thing you see is our navbar component's function. I'm using the usePathname() hook provided by Next.js to get the active pathname. The next thing is our actual UI code styled using Tailwind CSS. I'm mapping over the navItems array and returning Next.js's in-built Link component for navigation. Notice I'm conditionally setting the text color of the links based on whether the link is active or not. The link activity can be checked by seeing if the path is equal to our active pathname we get from usePathname() hook.

Creating the layout

Now that our basic navbar component is done, let's create our layout file and add the navbar component to it so that each page in our web app has access to the navbar. Look for the layout.tsx file in the root of your app folder and change the code to this.



// src/app/layout.tsx

import "./globals.css";
import NavBar from "@/components/navbar";

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className="bg-gradient-to-tr overflow-x-hidden min-w-screen from-zinc-950 via-stone-900 to-neutral-950 flex min-h-screen flex-col items-center justify-between">
        <main className="p-4 py-24 gap-6 w-full lg:w-[55%]">
          <section className="w-full flex gap-4 justify-start mb-6 p-2">
            <div>
              <img
                src="https://avatars.githubusercontent.com/u/68690233?s=100&v=4"
                alt="avatar"
                className="w-12 h-12 rounded-full shadow-lg grayscale hover:grayscale-0 duration-300"
              />
            </div>
            <div className="flex flex-col gap-2 justify-center">
              <h2 className="mb-0 text-zinc-100 font-bold">Ashish</h2>
              <p className="mb-0 text-zinc-400 font-semibold leading-none">
                Student  Dev  Ailurophile
              </p>
            </div>
          </section>
          <NavBar />
          {children}
        </main>
      </body>
    </html>
  );
}


Enter fullscreen mode Exit fullscreen mode

Let's break down the code. The first thing you need to do is import your Navbar component in your layout import NavBar from "@/components/navbar";. The "@" here is an import alias you can set while scaffolding your app. The next thing you would see is the metadata object. Don't worry if you don't understand what it is, it's used to define the metadata for your pages - for now let's not change it.

The last thing is our actual RootLayout function or global layout (global as it applies to all of your routes). I have added some basic styles for the layout along with an header section with my name and bio on it. The navbar component is added right below it. Now, if you head over to localhost:3000 after starting the dev server, you would be able to see a basic page like this with the navbar on it.

Adding the animations to navbar

Here comes the last step of this guide. We will be adding the hover animations inspired from Vercel to our navbar. Here's what the navbar component will look like after adding the animation code -



// src/app/components/Navbr.tsx

"use client";

import { motion } from "framer-motion";
import { useState } from "react";
import { usePathname } from "next/navigation";
import Link from "next/link";

const navItems = [
  {
    path: "/",
    name: "Home",
  },
  {
    path: "/now",
    name: "Now",
  },
  {
    path: "/guestbook",
    name: "Guestbook",
  },
  {
    path: "/writing",
    name: "Writing",
  },
];

export default function NavBar() {
  let pathname = usePathname() || "/";

  if (pathname.includes("/writing/")) {
    pathname = "/writing";
  }

  const [hoveredPath, setHoveredPath] = useState(pathname);

  return (
    <div className="border border-stone-800/90 p-[0.4rem] rounded-lg mb-12 sticky top-4 z-[100] bg-stone-900/80 backdrop-blur-md">
      <nav className="flex gap-2 relative justify-start w-full z-[100]  rounded-lg">
        {navItems.map((item, index) => {
          const isActive = item.path === pathname;
          
          return (
            <Link
              key={item.path}
              className={`px-4 py-2 rounded-md text-sm lg:text-base relative no-underline duration-300 ease-in ${
                isActive ? "text-zinc-100" : "text-zinc-400"
              }`}
              data-active={isActive}
              href={item.path}
              onMouseOver={() => setHoveredPath(item.path)}
              onMouseLeave={() => setHoveredPath(pathname)}
            >
              <span>{item.name}</span>
              {item.path === hoveredPath && (
                <motion.div
                  className="absolute bottom-0 left-0 h-full bg-stone-800/80 rounded-md -z-10"
                  layoutId="navbar"
                  aria-hidden="true"
                  style={{
                    width: "100%",
                  }}
                  transition={{
                    type: "spring",
                    bounce: 0.25,
                    stiffness: 130,
                    damping: 9,
                    duration: 0.3,
                  }}
                />
              )}
            </Link>
          );
        })}
      </nav>
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

Since this code could look a bit complex to some, let's break it down to bits and understand what's happening here.

  1. Firstly, we need to add a few more imports - motion from framer-motion and useState hook from react.
  2. The second thing that's been added is this block of code, it's a simple trick I'm using to ignore the slug of my blog posts so that the active pathname is still /wriiting. ```tsx

if (pathname.includes("/writing/")) {
  pathname = "/writing";
}

3. The third thing is our state `hoveredPath`, it's being used to keep track of the link which is being hovered as we are using framer-motion and not simply css hover property.
4. The fourth thing that's been added is this code block to our Link component, The `onMouseOver` property is being used to set the pathname to the link's pathname whenever someone hovers over the link & the `onMouseLeave` property is being used to set the hovered pathname back to `"/"` after the cursor leaves the link. This is important as we don't want the background to be stuck on the same link even after we stop hovering over it.
```tsx


...
onMouseOver={() => setHoveredPath(item.path)}
onMouseLeave={() => setHoveredPath(pathname)}
...


Enter fullscreen mode Exit fullscreen mode
  1. The fith and the last thing added to our code is the motion component that will be used to show the hover animation. If you don't know what motion does, consider reading the framer-motion docs. For now, you can think of it as an animatable component. We are using the motion.div component here as we need a div component to act as the background for our links. You can also see that the motion component is being rendered conditionally whenever item.path === hoveredPath. So, whenever a link is hovered our motion div becomes visible creating the background effect! The motion div is being positioned relative to the Link component and has a z-index of -10 so that it appears below our Link component. It also has a styling of width 100% which would make sure it covers the whole Link component and the transition property which can be used to control the animation. You can play around with the values to see how they work. Don't forget to read the framer-motion docs though.

Note: Please remove all the styles from the Next.js default template from globals.css. Your CSS file should look like this -



/* /src/app/globals.css */

@tailwind base;
@tailwind components;
@tailwind utilities;


Enter fullscreen mode Exit fullscreen mode

With this, our animated navbar is complete and you can test it out in you dev server, Here's what it would look like if you did everything correctly -

final navbar

Clicking on other links would lead to a 404 as we haven't created the pages for the routes yet. After adding routes for those pages the navbar would work like this -

Conclusion

This might seem like "over-engineering" to some, but to me it's not as I was able to add a simple yet beautiful animation to my earlier boring-looking navbar by adding just a few lines of code.
If you find anything hard to understand or something you don't get, feel free to leave a comment - I'll get back to you. Thanks for reading :)

. . . . . . . . . . . . . . . . . . . . . . . . . .