使用 React (Next.js v13)、Framer-Motion 和 Tailwind CSS 创建一个受 Vercel 启发的动画导航栏。
在开发 Web 应用时,我通常喜欢从其他网站汲取灵感。我总是会被那些设计精美、动画效果巧妙的网站所吸引。Vercel就是我作为一名 Web 开发者非常喜欢的网站之一。此外,Vercel 的设计和前端团队也是业内顶尖的。几天前,当我在 Vercel 上部署我的一个应用时,我注意到他们的导航栏组件有一个非常流畅的悬停动画。具体来说,就是每个导航栏链接/标签都有一个背景颜色,当鼠标悬停在导航栏上时,背景颜色会跟随鼠标移动。由于我目前正在开发个人网站的下一个版本(使用 Next.js v13),所以我决定也在我的网站上实现这个功能。你可以把这篇文章看作是创建导航栏的指南!以下是导航栏的外观和工作原理 -
第一步
我之前已经提到过,这个网站是用 Next.js v13 构建的。所以,首先你需要做的就是使用这个命令搭建一个 Next.js 应用。搭建过程中,系统会提示你是否要将 Tailwind 添加到项目中,请确认添加位置,并确保你使用的是正确的应用目录和/src文件夹,这样我们才能在工作中保持一致。
pnpm create next-app@latest
下一步是安装所需的依赖项,在本例中主要是 Framer-Motion -
pnpm i framer-motion
启动开发服务器 -
pnpm dev
现在我们的基本项目已经准备就绪,我们可以开始构建导航栏了!
构建导航栏组件
我们先来创建基本的导航栏组件,并将其添加到项目的全局布局文件中。如果您还不了解布局文件,它其实是 Next.js v13 中引入的一种新文件类型,可以用来创建网站的布局。默认情况下,Next.js 会在应用程序的根目录下创建一个名为 `.js` 的布局文件layout.tsx。
/components在你的目录下创建一个文件夹/app,并在该文件夹内创建一个文件Navbar.tsx。
// 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>
);
}
这是我们可以使用的基本导航栏组件。如果您不理解文件顶部的“use client”是什么意思,您需要阅读 Next.js 文档并了解一些 React 服务器组件的相关知识。简而言之,Next.js v13 中的所有 React 组件默认都是服务器组件,这意味着它们运行在服务器端。因此,如果您想使用客户端的 Hook,例如 `<input type="navbar">`useState或useEffect`<input type="navbar">`,则需要使用“use client”这行代码将它们转换为客户端组件。
现在我们来分析剩下的代码。首先,我定义了一个数组,其中包含我想在导航栏上显示的项目。在我的例子中,它们是:,,,,/和。接下来你会看到导航栏组件的函数。我使用了Next.js 提供的钩子来获取当前激活的路径名。然后是使用 Tailwind CSS 设置样式的实际 UI 代码。我遍历数组并返回 Next.js 内置的导航组件。注意,我根据链接是否处于激活状态来有条件地设置链接的文本颜色。可以通过检查路径是否等于我们从钩子中获取的激活路径名来判断链接是否处于激活状态。/now/guestbook/writingusePathname()navItemsLinkusePathname()
创建布局
现在我们的基本导航栏组件已经完成,接下来让我们创建布局文件并将导航栏组件添加到其中,以便 Web 应用中的每个页面都能访问导航栏。在layout.ts应用文件夹的根目录下找到 x 文件,并将代码修改为以下内容。
// 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>
);
}
让我们来分析一下代码。首先,你需要将 Navbar 组件导入到布局文件中import NavBar from "@/components/navbar";。这里的“@”是一个导入别名,你可以在搭建应用时设置它。接下来你会看到一个metadata对象。如果你不理解它的作用,不用担心,它是用来定义页面元数据的——现在我们暂时不要修改它。
最后一步是实际的RootLayout功能或全局布局(全局布局是指它适用于所有路由)。我为布局添加了一些基本样式,以及一个包含我的名字和个人简介的头部区域。导航栏组件就添加到头部下方。现在,如果您localhost:3000在启动开发服务器后访问该页面,您将能够看到一个带有导航栏的基本页面。
为导航栏添加动画
本指南的最后一步来了。我们将为导航栏添加受 Vercel 启发而设计的悬停动画。以下是添加动画代码后导航栏组件的最终效果——
// 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>
);
}
由于这段代码对某些人来说可能看起来有点复杂,让我们把它分解成几个部分,了解这里发生了什么。
- 首先,我们需要添加一些导入——
motion来自 framer-motion 和useState来自 react 的 hook。 - 其次添加的是这段代码,这是一个简单的技巧,我用它来忽略博客文章的别名,以便活动路径名仍然是
/wriiting。
if (pathname.includes("/writing/")) {
pathname = "/writing";
}
- 第三点是我们的状态
hoveredPath,它用于跟踪悬停的链接,因为我们使用的是 framer-motion 而不是简单的 css hover 属性。 - 第四项新增内容是添加到 Link 组件中的这段代码块。该
onMouseOver属性用于在鼠标悬停在链接上时将路径名设置为链接的实际路径名,并在鼠标移开链接后将onMouseLeave悬停路径名恢复为默认值"/"。这一点很重要,因为我们不希望即使鼠标移开后,背景仍然停留在同一个链接上。
...
onMouseOver={() => setHoveredPath(item.path)}
onMouseLeave={() => setHoveredPath(pathname)}
...
- 添加到代码中的第五个也是最后一个组件是
motion用于显示悬停动画的组件。如果您不了解它的作用motion,请阅读 framer-motion 的文档。现在,您可以将其视为一个可动画的组件。我们在motion.div这里使用这个组件是因为我们需要一个 div 组件作为链接的背景。您还可以看到,当鼠标悬停在链接上时,动画组件会根据条件渲染item.path === hoveredPath。因此,当鼠标悬停在链接上时,动画 div 就会显示出来,从而产生背景效果!动画 div 相对于链接组件进行定位,并且 z-index 设置为 -10,使其显示在链接组件下方。它的宽度样式设置为 100%,以确保它覆盖整个链接组件,并且它还具有 transition 属性,可用于控制动画。您可以尝试不同的值来查看它们的效果。不过,别忘了阅读 framer-motion 的文档。
注意:请移除 Next.js 默认模板中的所有样式globals.css。您的 CSS 文件应如下所示:
/* /src/app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
至此,我们的动画导航栏就完成了,你可以在开发服务器上进行测试。如果一切操作正确,它应该看起来像这样——
点击其他链接会显示 404 错误,因为我们还没有创建这些路由对应的页面。添加这些页面的路由后,导航栏会像这样工作:
结论
有些人可能觉得这是“过度设计”,但对我来说并非如此,因为我只添加了几行代码,就为之前略显单调的导航栏添加了一个简洁而美观的动画效果。
如果您有任何难以理解或不明白的地方,请随时留言——我会尽快回复。感谢阅读 :)

