Navbar
Navbar 从 HeroUI v2 到 v3 的迁移指南。
HeroUI v3 已移除 Navbar 组件。请使用原生 HTML 元素与 Tailwind CSS 类手动搭建导航栏。本指南介绍常见模式,并简化复杂能力的说明。
关键变化
1. 组件移除
v2: <Navbar> 与子组件(NavbarBrand、NavbarContent、NavbarItem、NavbarMenu、NavbarMenuItem、NavbarMenuToggle)
v3: 使用原生 HTML 手动组合
2. 子组件映射
| v2 组件 | v3 对应 | 说明 |
|---|---|---|
Navbar | <nav> 元素 | 主容器 |
NavbarBrand | <div> 或 <a> | Logo / 品牌区 |
NavbarContent | <ul> 元素 | 导航列表 |
NavbarItem | <li> 元素 | 导航项 |
NavbarMenu | 移动端菜单覆盖层 | 需自行实现 |
NavbarMenuItem | 移动菜单内的 <li> | 移动菜单项 |
NavbarMenuToggle | <button> | 移动菜单切换 |
3. 已移除的能力
shouldHideOnScroll— 需自行实现滚动检测isBlurred— 请使用 Tailwind CSS 的backdrop-blur工具类isBordered— 请使用 Tailwind CSS 边框类position变体 — 请使用 Tailwind CSS 的sticky、fixed等类maxWidth变体 — 请使用 Tailwind CSS 的max-w-*类- 移动菜单动画 — 需自行实现
- 滚动锁定 — 需自行实现(见下文「滚动锁定」)
迁移示例
基础 Navbar
import { Navbar, NavbarBrand, NavbarContent, NavbarItem, Link, Button } from "@lenso/ui";
{/* Basic */}<Navbar> <NavbarBrand> <Logo /> <p className="font-bold">ACME</p> </NavbarBrand> <NavbarContent> <NavbarItem><Link href="#">Features</Link></NavbarItem> <NavbarItem><Link href="#">Pricing</Link></NavbarItem> </NavbarContent></Navbar>
{/* With right-aligned content */}<Navbar> <NavbarBrand>Logo</NavbarBrand> <NavbarContent justify="end"> <NavbarItem><Button>Sign Up</Button></NavbarItem> </NavbarContent></Navbar>移动菜单(简化版)
import { Navbar, NavbarBrand, NavbarContent, NavbarItem, NavbarMenu, NavbarMenuItem, NavbarMenuToggle,} from "@lenso/ui";
function App() { const [isMenuOpen, setIsMenuOpen] = useState(false);
return ( <Navbar onMenuOpenChange={setIsMenuOpen}> <NavbarContent> <NavbarMenuToggle className="sm:hidden" /> <NavbarBrand>Logo</NavbarBrand> </NavbarContent> <NavbarContent className="hidden md:flex"> <NavbarItem>Features</NavbarItem> <NavbarItem>Pricing</NavbarItem> </NavbarContent> <NavbarMenu> <NavbarMenuItem>Features</NavbarMenuItem> <NavbarMenuItem>Pricing</NavbarMenuItem> </NavbarMenu> </Navbar> );}完整示例
import { Navbar, NavbarBrand, NavbarContent, NavbarItem, NavbarMenu, NavbarMenuItem, NavbarMenuToggle, Link, Button,} from "@lenso/ui";
export default function App() { const [isMenuOpen, setIsMenuOpen] = useState(false);
return ( <Navbar position="sticky" maxWidth="lg" onMenuOpenChange={setIsMenuOpen}> <NavbarContent> <NavbarMenuToggle className="sm:hidden" /> <NavbarBrand> <Logo /> <p className="font-bold">ACME</p> </NavbarBrand> </NavbarContent> <NavbarContent className="hidden md:flex"> <NavbarItem> <Link href="#">Features</Link> </NavbarItem> <NavbarItem isActive> <Link href="#">Dashboard</Link> </NavbarItem> <NavbarItem> <Link href="#">Pricing</Link> </NavbarItem> </NavbarContent> <NavbarContent justify="end"> <NavbarItem> <Link href="#">Login</Link> </NavbarItem> <NavbarItem> <Button>Sign Up</Button> </NavbarItem> </NavbarContent> <NavbarMenu> <NavbarMenuItem> <Link href="#">Features</Link> </NavbarMenuItem> <NavbarMenuItem> <Link href="#">Dashboard</Link> </NavbarMenuItem> <NavbarMenuItem> <Link href="#">Pricing</Link> </NavbarMenuItem> </NavbarMenu> </Navbar> );}创建可复用的 Navbar 组件(推荐)
导航栏在应用中很常见,下面是一个简化的可复用组件示例:
import { useState, ReactNode } from "react";import { Link, Button } from "@lenso/ui";import { cn } from "@/lib/utils"; // or your cn utility
interface NavbarItem { label: string; href: string; isActive?: boolean;}
interface NavbarProps { brand: ReactNode; items: NavbarItem[]; rightContent?: ReactNode; className?: string; maxWidth?: "sm" | "md" | "lg" | "xl" | "2xl" | "full"; position?: "static" | "sticky" | "fixed";}
const maxWidthClasses = { sm: "max-w-[640px]", md: "max-w-[768px]", lg: "max-w-[1024px]", xl: "max-w-[1280px]", "2xl": "max-w-[1536px]", full: "max-w-full",};
export function Navbar({ brand, items, rightContent, className, maxWidth = "lg", position = "sticky",}: NavbarProps) { const [isMenuOpen, setIsMenuOpen] = useState(false);
return ( <nav className={cn( "z-40 w-full border-b border-separator bg-background/70 backdrop-blur-lg", position === "sticky" && "sticky top-0", position === "fixed" && "fixed top-0", className )} > <header className={cn( "flex h-16 items-center justify-between px-6", maxWidth !== "full" && maxWidthClasses[maxWidth], "mx-auto" )} > <div className="flex items-center gap-4"> <button className="md:hidden" onClick={() => setIsMenuOpen(!isMenuOpen)} aria-label="Toggle menu" aria-expanded={isMenuOpen} > <span className="sr-only">Menu</span> <svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> {isMenuOpen ? ( <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> ) : ( <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /> )} </svg> </button> {brand} </div> <ul className="hidden items-center gap-4 md:flex"> {items.map((item) => ( <li key={item.href}> <Link href={item.href} className={cn(item.isActive && "font-medium text-accent")} aria-current={item.isActive ? "page" : undefined} > {item.label} </Link> </li> ))} </ul> {rightContent && <div className="hidden items-center gap-4 md:flex">{rightContent}</div>} </header> {isMenuOpen && ( <div className="border-t border-separator md:hidden"> <ul className="flex flex-col gap-2 p-4"> {items.map((item) => ( <li key={item.href}> <Link href={item.href} className={cn( "block py-2", item.isActive && "font-medium text-accent" )} > {item.label} </Link> </li> ))} {rightContent && ( <li className="mt-4 flex flex-col gap-2 border-t border-separator pt-4"> {rightContent} </li> )} </ul> </div> )} </nav> );}
// Usage<Navbar brand={ <> <Logo /> <p className="font-bold">ACME</p> </> } items={[ { label: "Features", href: "#features" }, { label: "Dashboard", href: "#dashboard", isActive: true }, { label: "Pricing", href: "#pricing" }, ]} rightContent={ <> <Link href="#login">Login</Link> <Button>Sign Up</Button> </> }/>高级能力(需自行实现)
滚动时隐藏
shouldHideOnScroll 需要自行实现滚动检测:
import { useState, useEffect } from "react";
function useScrollDirection() { const [isHidden, setIsHidden] = useState(false); const [lastScrollY, setLastScrollY] = useState(0);
useEffect(() => { const handleScroll = () => { const currentScrollY = window.scrollY; setIsHidden(currentScrollY > lastScrollY && currentScrollY > 64); setLastScrollY(currentScrollY); };
window.addEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll); }, [lastScrollY]);
return isHidden;}
function NavbarWithHideOnScroll() { const isHidden = useScrollDirection();
return ( <nav className={cn( "sticky top-0 z-40 w-full transition-transform duration-300", isHidden && "-translate-y-full" )} > {/* navbar content */} </nav> );}滚动锁定
在移动菜单打开时锁定页面滚动:
useEffect(() => { if (isMenuOpen) { document.body.style.overflow = "hidden"; } else { document.body.style.overflow = ""; } return () => { document.body.style.overflow = ""; };}, [isMenuOpen]);总结
- 组件已移除:
Navbar、NavbarBrand、NavbarContent、NavbarItem、NavbarMenu、NavbarMenuItem、NavbarMenuToggle均已移除。 - 导入调整:从
@lenso/ui中移除所有 Navbar 相关导入。 - 手动组合:使用原生 HTML 搭建导航栏。
- 移动菜单:通过状态管理手动实现移动菜单。
- 样式:直接使用 Tailwind CSS 类。
- 高级能力:滚动时隐藏、动画等需自行实现。
迁移步骤
- 移除导入:删除所有与 Navbar 相关的导入。
- 替换结构:将
<Navbar>替换为<nav>。 - 替换子组件:用语义化 HTML(
<header>、<ul>、<li>)替代子组件。 - 补充移动菜单:手动实现菜单切换与菜单面板。
- 应用样式:用 Tailwind CSS 完成布局与样式。
- 管理状态:用 React
useState管理移动菜单开关。 - (可选) 为应用封装可复用的 Navbar 组件。
常见模式
简单导航
<nav className="sticky top-0 z-40 w-full border-b border-separator bg-background"> <header className="flex h-16 items-center justify-between px-6"> <div>Logo</div> <ul className="flex items-center gap-4"> <li><Link href="#">Home</Link></li> <li><Link href="#">About</Link></li> <li><Link href="#">Contact</Link></li> </ul> </header></nav>搭配下拉(使用 v3 Dropdown)
import { Dropdown, Button, Label } from "@lenso/ui";
<ul className="flex items-center gap-4"> <li> <Dropdown> <Button variant="ghost">Features</Button> <Dropdown.Popover> <Dropdown.Menu> <Dropdown.Item id="feature1" textValue="Feature 1"> <Label>Feature 1</Label> </Dropdown.Item> <Dropdown.Item id="feature2" textValue="Feature 2"> <Label>Feature 2</Label> </Dropdown.Item> </Dropdown.Menu> </Dropdown.Popover> </Dropdown> </li></ul>