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 -
pnpm create next-app@latest
The next thing would be to install the dependencies required, mainly Framer-Motion in this case -
pnpm i framer-motion
Start the dev server -
pnpm dev
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>
);
}
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.ts
x 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>
);
}
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>
);
}
Since this code could look a bit complex to some, let's break it down to bits and understand what's happening here.
- Firstly, we need to add a few more imports -
motion
from framer-motion anduseState
hook from react. - 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";
}
- 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. - 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 & theonMouseLeave
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)}
...
- 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 whatmotion
does, consider reading the framer-motion docs. For now, you can think of it as an animatable component. We are using themotion.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 wheneveritem.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;
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 -
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 (20)
I am going to leave some friendly comments for writing updates that i think could really make a difference to the readers :)
thanks for all the suggestions, this is why i love the dev community <3
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?
actually import aliases are kinda newly added to next.js, wasn't sure if everyone would know this
Is 3 years ago newish though? Aliases have been around since around 2016.
nextjs.org/blog/next-9-4
the option to change/add import alias through the cli itself was added recently, my bad
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.
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:
noted !
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 :)
makes sense, adding the video to top as well :)
Well done. Thanks for sharing
Any github links will be useful.
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?
Nice!!!
Can you make vercel headless to make it compatible with turbo
Why is it not compatible with turbo? can you elaborate what you're trying to say please?
This's great post, thanks for sharing
This is a great blog. witchcraft spells for love
Some comments may only be visible to logged-in visitors. Sign in to view all comments.