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

ashish @asheeshh

About: student and solo dev by passion who likes writing occasionally

Location:
Under a Blanket
Joined:
Oct 6, 2021

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

Publish Date: Aug 9 '23
174 24

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.
if (pathname.includes("/writing/")) {
   pathname = "/writing";
}
Enter fullscreen mode Exit fullscreen mode
  1. 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.
  2. 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.
...
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 :)

Comments 24 total

  • GreggHume
    GreggHumeAug 10, 2023

    I am going to leave some friendly comments for writing updates that i think could really make a difference to the readers :)

    • GreggHume
      GreggHumeAug 10, 2023

      I find it easier to read articles if some of the code snippets are grouped rather than split up. Seeing as a user must run all of these in sequence its nice to see them together.

      # create next app
      pnpm create next-app@latest
      
      # install framer
      pnpm i framer-motion
      
      # run dev server
      pnpm dev
      
      Enter fullscreen mode Exit fullscreen mode
    • GreggHume
      GreggHumeAug 10, 2023

      Its nice to show the folder and file structure up front and tell readers what folders and files to create:

      Create the layout and navbar components like so:

      ├── app
          ├── layout.tsx
          ├── globals.css
          ├── components
              ├── Navbar.tsx
      
      Enter fullscreen mode Exit fullscreen mode
    • GreggHume
      GreggHumeAug 10, 2023

      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.

      I dont think you need to explain imports to developers, i think developers working with next should know imports and aliases. Unless you have experienced developers wanting to know this?

      • ashish
        ashish Aug 10, 2023

        actually import aliases are kinda newly added to next.js, wasn't sure if everyone would know this

        • GreggHume
          GreggHumeAug 10, 2023

          Is 3 years ago newish though? Aliases have been around since around 2016.
          nextjs.org/blog/next-9-4

          • ashish
            ashish Aug 10, 2023

            the option to change/add import alias through the cli itself was added recently, my bad

    • GreggHume
      GreggHumeAug 10, 2023

      It would be nice if you put a gif or the video of how this works first so we can see what this is about so we can decide if we want to read further - rather than having the video at the end :)

      • ashish
        ashish Aug 10, 2023

        makes sense, adding the video to top as well :)

    • ashish
      ashish Aug 10, 2023

      thanks for all the suggestions, this is why i love the dev community <3

  • Adetimmy
    AdetimmyAug 11, 2023

    Nice!!!

  • Manoj
    ManojAug 12, 2023

    Any github links will be useful.

    • ashish
      ashish Aug 12, 2023

      actually, the code for v2 of my personal site is private as it's a wip. but i can create one with only the navbar for you if you need, you need the code for the entire thing or something specific?

  • Phuong Danh
    Phuong DanhAug 12, 2023

    This's great post, thanks for sharing

  • Ethereum
    Ethereum Aug 13, 2023

    Can you make vercel headless to make it compatible with turbo

    • ashish
      ashish Aug 13, 2023

      Why is it not compatible with turbo? can you elaborate what you're trying to say please?

  • px
    pxAug 17, 2023

    Well done. Thanks for sharing

  • Isaac Johnson
    Isaac JohnsonSep 2, 2023

    FWIW, i had some errors resolving Navbar:

    Failed to compile.
    ./src/app/layout.tsx
    Module not found: Can't resolve '@/components/navbar'

    For me, needed to add 'app' to the path:

    import "./globals.css";
    import NavBar from "@/app/components/Navbar";
    
    Enter fullscreen mode Exit fullscreen mode
  • AJ
    AJSep 2, 2023

    Here is a pure HTML / CSS implementation. No javascript needed!
    codepen.io/koga73/pen/ZEVpzab?edit...

    • ashish
      ashish Sep 6, 2023

      impressive, there's a small bug though you cant move left when youre on about or contact tab, nice work anyways :)

  • YGN
    YGNJun 7, 2024

    Confused about the web dev can you help me out with the right roadmap

Add comment