Docs
Header

Header

网站导航头部组件

🚧
此组件正在施工中....

自动安装

使用以下命令之一自动安装组件,根据你的包管理器选择:

npx tofu-ui-cli@latest add header

手动安装 (可选)

注意
如果你使用手动安装,将无法获取更新

如果你需要手动安装,请在项目目录中创建如下路径文件:

components/ui/tofu/header.tsx

然后复制此代码:

"use client"
/**
 * TofuUI Header 头部导航组件
 * @author shuakami
 * @version 1.9.2
 * @copyright ByteFreeze&TofuUI
 */
 
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { SearchIcon, MenuIcon } from './svg';
// @ts-ignore
import { motion, AnimatePresence } from 'framer-motion';
import Search from './search';
// @ts-ignore
import { debounce } from 'lodash';
 
interface HeaderProps {
    logo: React.ReactNode;
    onClose?: () => void;
    menuItems: Array<{ label: string; url?: string; subMenus?: Array<{ label: string; items: Array<{ label: string; url: string }> }> }>;
}
 
 
const Header: React.FC<HeaderProps> = ({ logo, menuItems }) => {
    const [isVisible, setIsVisible] = useState(true);
    const [lastScrollY, setLastScrollY] = useState(0);
    const [activeSubMenu, setActiveSubMenu] = useState<number | null>(null);
    const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
    const [isTofuActive, setIsTofuActive] = useState(false);
    const [isSearchOpen, setIsSearchOpen] = useState(false);
    const [prevActiveSubMenu, setPrevActiveSubMenu] = useState<number | null>(null);
 
 
    const handleSubMenuOpen = debounce((index: React.SetStateAction<number | null>) => {
        setPrevActiveSubMenu(activeSubMenu);
        setActiveSubMenu(index);
        setIsTofuActive(true);
        setIsSearchOpen(false);
    }, 200);
 
    const handleSubMenuClose = debounce(() => {
        setActiveSubMenu(null);
        setIsTofuActive(false);
    }, 200);
 
    const handleMobileMenuToggle = () => {
        setIsMobileMenuOpen(!isMobileMenuOpen);
        setIsSearchOpen(false);
    };
 
 
    useEffect(() => {
        const handleScroll = () => {
            const currentScrollY = window.scrollY;
            setIsVisible(currentScrollY < lastScrollY || currentScrollY < 200);
            setLastScrollY(currentScrollY);
        };
 
        window.addEventListener('scroll', handleScroll);
        return () => window.removeEventListener('scroll', handleScroll);
    }, [lastScrollY]);
 
    const toggleMobileMenu = () => {
        setIsMobileMenuOpen(!isMobileMenuOpen);
        setIsTofuActive(!isMobileMenuOpen);
    };
 
    const handleCloseSearch = () => {
        setIsSearchOpen(false);
    };
 
 
    const headerVariants = {
        visible: { opacity: 1, y: 0 },
        hidden: { opacity: 0, y: -100 },
    };
 
    useEffect(() => {
        const tofuContainer = document.querySelector('.tofu-container');
        if (tofuContainer) {
            if (isTofuActive || activeSubMenu !== null) {
                tofuContainer.classList.add('home-tofu');
            } else {
                tofuContainer.classList.remove('home-tofu');
            }
        }
    }, [isTofuActive, activeSubMenu]);
 
 
 
 
    return (
        <AnimatePresence>
            <motion.header
                variants={headerVariants}
                initial="visible"
                animate={isVisible ? 'visible' : 'hidden'}
                exit="hidden"
                transition={{ duration: 0.3, ease: 'easeInOut' }}
                className={`w-full flex items-center min-h-[3rem] justify-between fixed top-0 left-0 right-0 z-20 px-4 py-4 md:px-8`}
            >
                <motion.div
                    className="absolute inset-0 bg-tofu-light-bg/80 dark:bg-tofu-dark-bg/50 backdrop-blur-xl"
                    initial={{ opacity: 0, backdropFilter: 'blur(0px)' }}
                    animate={activeSubMenu !== null || isMobileMenuOpen || isSearchOpen ? 'hidden' : 'visible'}
                    variants={{
                        visible: { opacity: 1, backdropFilter: 'blur(12px)' },
                        hidden: { opacity: 0, backdropFilter: 'blur(0px)' },
                    }}
                    transition={{ duration: 0.3, ease: 'easeInOut' }}
                />
 
                <div className="max-w-7xl mx-auto w-full flex items-center justify-between">
                    <Link href="/" aria-label="TofuUI Home"
                          className="transition ease-in-out duration-250 scale-85 origin-left">
                        {logo}
                    </Link>
 
                    <div className="flex items-center justify-center gap-5 md:hidden scale-85 origin-right">
                        <button aria-label="Open Search"
                                className="group relative w-5 h-5 transition-opacity duration-300 ease-in-out focus:outline-none">
                            <SearchIcon/>
                        </button>
 
                        <button
                            className="w-5 h-5 flex flex-col items-center justify-center gap-1 z-30"
                            aria-controls="main-menu"
                            aria-label="Open Menu"
                            onClick={toggleMobileMenu}
                        >
                            <MenuIcon/>
                        </button>
                    </div>
                    {isSearchOpen && (
                        <Search onClose={handleCloseSearch}/>
                    )}
                    <AnimatePresence>
                        {isMobileMenuOpen && (
                            <motion.div className="fixed inset-0 dark:bg-tofu-dark-bg/70 transition-bg duration-500">
                                <motion.div
                                    className="-z-10 h-screen "
                                    initial={{backdropFilter: 'blur(0px)'}}
                                    animate={{
                                        backdropFilter: 'blur(24px)',
                                        transition: {
                                            duration: 0.6,
                                            ease: [0.83, 0, 0.17, 1],
                                        },
                                    }}
                                    exit={{
                                        backdropFilter: 'blur(0px)',
                                        transition: {
                                            duration: 0.6,
                                            ease: [0.83, 0, 0.17, 1],
                                        },
                                    }}
                                    style={{willChange: 'backdrop-filter'}}
                                >
                                    <motion.div
                                        initial={{opacity: 0}}
                                        animate={{
                                            opacity: 1,
                                            transition: {
                                                duration: 0.6,
                                                ease: [0.83, 0, 0.17, 1],
                                            },
                                        }}
                                        exit={{
                                            opacity: 0,
                                            transition: {
                                                duration: 0.4,
                                                ease: [0.83, 0, 0.17, 1],
                                            },
                                        }}
                                    >
                                        <div className="flex items-center justify-between px-6 py-4">
                                            <Link href="/" aria-label="TofuUI Home"
                                                  className="transition duration-250 ease-in-out">
                                                {logo}
                                            </Link>
                                            <button
                                                className="w-5 h-5 flex flex-col items-center justify-center gap-1"
                                                aria-label="Close Menu"
                                                onClick={toggleMobileMenu}
                                            >
                                                <MenuIcon/>
                                            </button>
                                        </div>
                                        <ul className="flex flex-col w-full max-w-container mx-auto pt-4 pb-6 text-lg"
                                            id="main-menu">
                                            {menuItems.map((item, index) => (
                                                <li key={index} className="group last:pb-10">
                                                    <button
                                                        className="py-2 w-full flex items-center justify-between px-6 text-gray-800 dark:text-white"
                                                        aria-expanded={activeSubMenu === index}
                                                        onClick={() => setActiveSubMenu(activeSubMenu === index ? null : index)}
                                                    >
                                                        <span className="text-2xl">{item.label}</span>
                                                        <svg
                                                            xmlns="http://www.w3.org/2000/svg"
                                                            className={` transition-all duration-300 ease-in-out ${
                                                                activeSubMenu === index ? 'rotate-180' : ''
                                                            }`}
                                                            width="16"
                                                            viewBox="0 0 16 9"
                                                            fill="none"
                                                        >
                                                            <path
                                                                fillRule="evenodd"
                                                                clipRule="evenodd"
                                                                d="M0.292893 0.292893C0.683418 -0.0976311 1.31658 -0.0976311 1.70711 0.292893L8 6.58579L14.2929 0.292894C14.6834 -0.0976305 15.3166 -0.0976304 15.7071 0.292894C16.0976 0.683418 16.0976 1.31658 15.7071 1.70711L8.70711 8.70711C8.31658 9.09763 7.68342 9.09763 7.29289 8.70711L0.292893 1.70711C-0.0976311 1.31658 -0.0976311 0.683417 0.292893 0.292893Z"
                                                                fill="currentColor"
                                                            />
                                                        </svg>
                                                    </button>
                                                    {item.subMenus && (
                                                        <ul
                                                            className={`pt-2 overflow-hidden transition-all duration-500 ease-in-out ${
                                                                activeSubMenu === index ? 'max-h-[50rem] opacity-100' : 'max-h-0 opacity-0 invisible'
                                                            }`}
                                                        >
                                                            {item.subMenus.map((subMenu, subIndex) => (
                                                                <li key={subIndex} className="mb-7 mt-5">
                                                        <span
                                                            className="block px-6 text-lg text-tofu-black-ds dark:text-tofu-dark-header-ds mb-1">
                                                            {subMenu.label}
                                                        </span>
                                                                    <ul className="pl-6">
                                                                        {subMenu.items.map((item, itemIndex) => (
                                                                            <li key={itemIndex} className="mb-1 mt-2">
                                                                                <Link
                                                                                    href={item.url}
                                                                                    className="text-md  transition duration-500 ease-in-out hover:text-gray-500 text-gray-600 dark:text-white"
                                                                                >
                                                                                    {item.label}
                                                                                </Link>
                                                                            </li>
                                                                        ))}
                                                                    </ul>
                                                                </li>
                                                            ))}
                                                        </ul>
                                                    )}
                                                </li>
                                            ))}
                                        </ul>
                                    </motion.div>
                                </motion.div>
                            </motion.div>
                        )}
                    </AnimatePresence>
 
 
                    <nav className="hidden md:block scale-85 origin-right">
                        <ul className="flex items-center gap-7">
                            {menuItems.map((item, index) => (
                                <motion.li
                                    key={index}
                                    className="flex items-center"
                                    onHoverStart={() => handleSubMenuOpen(index)}
                                    onHoverEnd={handleSubMenuClose}
                                >
                                    <Link
                                        href={item.url || '/'}
                                        className="text-sm relative flex items-center hover:text-black dark:hover:text-white transition ease-in duration-500 focus:outline-none"
                                    >
                                        <span>{item.label}</span>
                                    </Link>
                                    {/* 二级菜单 */}
                                    <AnimatePresence>
                                        {item.subMenus && activeSubMenu === index && (
                                            <motion.div
                                                className="absolute top-[0rem] left-0 w-full"
                                                onHoverEnd={() => setActiveSubMenu(null)}
                                                initial="closed"
                                                animate={activeSubMenu === index ? 'open' : 'closed'}
                                                exit="closed"
                                                variants={{
                                                    open: { opacity: 1, y: 0 },
                                                    closed: { opacity: 0, y: -20 },
                                                }}
                                                transition={{ duration: 0.3, ease: 'easeInOut' }}
                                            >
                                                <div className="flex justify-center pt-12 pb-16 mt-10 ml-12">
                                                    <motion.div
                                                        className="w-full mx-96 2xl:mx-96 xl:mx-60 md:mx-18 sm:mx-32 max-w-7xl py-3 grid grid-cols-1 md:grid-cols-3 gap-8 px-4 h-full"
                                                        key={activeSubMenu}
                                                        initial={prevActiveSubMenu !== null ? {
                                                            opacity: 0,
                                                            x: prevActiveSubMenu < activeSubMenu ? '100%' : '-100%'
                                                        } : {opacity: 0}}
                                                        animate={{opacity: 1, x: '0%'}}
                                                        exit={prevActiveSubMenu !== null ? {
                                                            opacity: 0,
                                                            x: prevActiveSubMenu > activeSubMenu ? '-100%' : '100%'
                                                        } : {opacity: 0}}
                                                        transition={{duration: 0.3, ease: 'easeInOut'}}
                                                    >
                                                        {item.subMenus.map((subMenu, subIndex) => (
                                                            <div key={subIndex} className="flex flex-col items-start">
                                                                <h3 className="text-sm font-semibold mb-3 dark:text-tofu-dark-header-ds  ">{subMenu.label}</h3>
                                                                <ul className="space-y-3 mt-1">
                                                                    {subMenu.items.map((item, itemIndex) => (
                                                                        <li key={itemIndex}>
                                                                            <Link
                                                                                href={item.url}
                                                                                className="text-xs dark:tofu-light transition ease-curve-a duration-250 focus-visible:rounded-s hover:text-copy-primary"
                                                                            >
                                                                                {item.label}
                                                                            </Link>
                                                                        </li>
                                                                    ))}
                                                                </ul>
                                                            </div>
                                                        ))}
                                                    </motion.div>
                                                </div>
 
                                                <motion.div
                                                    className="transform-gpu absolute top-0 left-0 w-full h-screen pointer-events-none -z-10 bg-tofu-light-bg/80 dark:bg-tofu-dark-bg/50 "
                                                    initial={{opacity: 1}}
                                                    animate={{
                                                        opacity: 1,
                                                        transition: {
                                                            duration: 0.4,
                                                            ease: 'easeInOut',
                                                        },
                                                    }}
                                                    exit={{
                                                        opacity: 1,
                                                        transition: {
                                                            duration: 0.3,
                                                            ease: 'easeInOut',
                                                        },
                                                    }}
                                                    style={{
                                                        willChange: 'opacity',
                                                        WebkitBackdropFilter: 'blur(24px)',
                                                        backdropFilter: 'blur(24px)',
                                                    }}
                                                />
                                            </motion.div>
                                        )}
                                    </AnimatePresence>
                                </motion.li>
                            ))}
                        </ul>
                    </nav>
 
                    <button
                        className="group hidden md:block w-5 h-5 relative focus:outline-none scale-85 origin-right"
                        aria-label="Open Search"
                        onClick={() => setIsSearchOpen(true)}
                    >
                        <SearchIcon/>
                    </button>
                </div>
 
            </motion.header>
        </AnimatePresence>
    );
};
 
export default Header;

导入组件

import Header from '@/components/ui/tofu/header';

使用组件

import Header from '@/components/ui/tofu/header';
 
const logo = (
<svg className="w-6 h-auto" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
  <path d="M26.153 11.46a6.888..." fill="currentColor"/>
</svg>
);
 
const menuItems = [
{ label: 'Home', url: '/' },
{ label: 'About', url: '/about' },
{ label: 'Services', subMenus: [
{ label: 'Consulting', items: [{ label: 'Business', url: '/services/business' }] }
  ]},
];
 
const App = () => (
<Header logo={logo} menuItems={menuItems} />
);
 
export default App;

示例

使用Header组件可以实现响应式导航栏,并支持多级下拉菜单。

Component header-styles-demo not found in registry.

参数说明

参数名类型默认值描述
logoReact.ReactNode必需显示在导航栏左侧的Logo
onClose() => void可选导航栏关闭时的回调函数
menuItemsArray<{ label: string; url?: string; subMenus?: Array<{ label: string; items: Array<{ label: string; url: string }> }> }>必需定义导航栏中的菜单项,支持无限级下拉菜单

教学专区

创建多级下拉菜单

Header组件中创建多级下拉菜单涉及到对menuItems属性的详细配置。下面的步骤和代码示例将逐一说明如何构建一个复杂的多级菜单结构。

定义顶层菜单

首先,定义顶层菜单项。这些是在导航栏中直接可见的项。

const menuItems = [
  {
    label: 'Products',
    url: '/products',
  },
  {
    label: 'Safety',
    url: '/safety',
  },
  {
    label: 'Company',
    url: '/company',
  },
];

在这个示例中,ProductsSafetyCompany是顶层菜单项,每个菜单项都有一个标签和一个指向相关页面的URL。

添加二级菜单

对于需要二级菜单的顶层菜单项,添加一个subMenus属性。这个属性是一个数组,包含了所有二级菜单项。

const menuItems = [
  {
    label: 'Products',
    url: '/products',
    subMenus: [
      {
        label: 'ChatGPT',
        items: [
          { label: 'For Everyone', url: '/chatgpt' },
          { label: 'For Teams', url: '/chatgpt/team' },
        ],
      },
      {
        label: 'API',
        items: [
          { label: 'Platform overview', url: '/api' },
        ],
      },
    ],
  },
];

Products下,我们有两个二级菜单项:ChatGPTAPI。每个二级菜单再包含具体的项,如For EveryonePlatform overview

配置更深层级的菜单项

如果需要,可以继续嵌套更多层级的菜单项。这通常适用于更复杂的导航结构。

const menuItems = [
  {
    label: 'Products',
    subMenus: [
      {
        label: 'ChatGPT',
        items: [
          { label: 'For Everyone', url: '/chatgpt' },
          {
            label: 'More Options',
            items: [
              { label: 'Advanced', url: '/chatgpt/advanced' },
            ],
          },
        ],
      },
    ],
  },
];