DEV Community

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

Posted on

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

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

Top comments (21)

Collapse
 
greggcbs profile image
GreggHume • Edited

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

Collapse
 
asheeshh profile image
ashish

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

Collapse
 
greggcbs profile image
GreggHume

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?

Collapse
 
asheeshh profile image
ashish

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

Thread Thread
 
greggcbs profile image
GreggHume • Edited

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

Thread Thread
 
asheeshh profile image
ashish

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

Collapse
 
greggcbs profile image
GreggHume • Edited

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
Collapse
 
asheeshh profile image
ashish

noted !

Collapse
 
greggcbs profile image
GreggHume • Edited

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
Collapse
 
greggcbs profile image
GreggHume

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

Collapse
 
asheeshh profile image
ashish

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

Collapse
 
manojadams profile image
Manoj

Any github links will be useful.

Collapse
 
asheeshh profile image
ashish

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?

Collapse
 
koga73 profile image
AJ

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

Collapse
 
asheeshh profile image
ashish

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

Collapse
 
pixl profile image
px

Well done. Thanks for sharing

Collapse
 
adetimmy_ profile image
Adetimmy

Nice!!!

Collapse
 
idjohnson profile image
Isaac Johnson

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
Collapse
 
sammyfilly profile image
Ethereum

Can you make vercel headless to make it compatible with turbo

Collapse
 
asheeshh profile image
ashish

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

Collapse
 
phuongdanh profile image
Phuong Danh

This's great post, thanks for sharing

Some comments may only be visible to logged-in visitors. Sign in to view all comments.