基于Next.js与Chakra UI实现多级Tab及对应功能
实现带平滑过渡的双层Tab组件(Next.js + Chakra UI)
需求实现方案
针对你需要的主分类切换子分类平滑过渡、子分类点击内容滚动聚焦的需求,以下是修改后的完整代码:
import { useState, useRef } from 'react'; import { Tabs, TabList, TabPanel, TabPanels, Box, Button, Tab } from '@chakra-ui/react'; import { motion, AnimatePresence } from 'framer-motion'; const MotionTab = motion(Tab); const MotionTabPanel = motion(TabPanel); const categories = [ { label: "パスタ", tooltip: "Main Pasta Menu", subcategories: ["OIL", "MEAT", "JAPANESE TASTE", "TOMATO", "ROASTED"] }, { label: "肉料理", tooltip: "Meat Dishes", subcategories: ["GRILLED", "FRIED", "ROASTED"] }, { label: "ドルチェ", tooltip: "Desserts", subcategories: ["CAKES", "PIES", "ICE CREAM"] }, { label: "キッズ", tooltip: "Kids Menu", subcategories: ["MEALS", "SNACKS"] }, { label: "プレ", tooltip: "Premium Menu", subcategories: ["SPECIAL DISH", "WINE PAIRINGS"] }, { label: "肉料理", tooltip: "Meat Dishes", subcategories: ["GRILLED", "FRIED", "ROASTED"] }, { label: "ドルチェ", tooltip: "Desserts", subcategories: ["CAKES", "PIES", "ICE CREAM"] }, { label: "キッズ", tooltip: "Kids Menu", subcategories: ["MEALS", "SNACKS"] }, ]; // 模拟内容数据,可替换为实际业务内容 const contentData = categories.map(category => category.subcategories.map(sub => `这是${category.label}分类下${sub}的详情内容...`.repeat(10)) ); export const TabMenu = () => { const [activeSubcategory, setActiveSubcategory] = useState({ tabIndex: 0, subIndex: null, }); const contentRefs = useRef<(HTMLDivElement | null)[]>([]); // 子分类点击后触发内容滚动聚焦 const handleSubcategoryClick = (tabIndex: number, subIndex: number) => { setActiveSubcategory({ tabIndex, subIndex }); setTimeout(() => { contentRefs.current[tabIndex * 10 + subIndex]?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 100); }; return ( <Tabs onChange={(index) => setActiveSubcategory({ tabIndex: index, subIndex: null })}> {/* 主分类Tab栏 */} <Box overflowX="auto" whiteSpace="nowrap" css={{ "&::-webkit-scrollbar": { display: "none" } }}> <TabList display="inline-flex" gap={0}> {categories.map((category, index) => ( <MotionTab as="div" key={index} _selected={{ bg: "brand.primary", color: "white", position: "relative", "::after": { content: `""`, position: "absolute", bottom: "-6px", left: "50%", transform: "translateX(-50%)", width: "0", height: "0", borderLeft: "6px solid transparent", borderRight: "6px solid transparent", borderTop: "8px solid brown.500", }, }} className='bold-small' color="mono.primary" whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} > {category.label} </MotionTab> ))} </TabList> </Box> {/* 子分类面板(带平滑过渡动画) */} <AnimatePresence mode="wait"> <TabPanels borderBottom="1px solid" borderColor="mono.divider" key={activeSubcategory.tabIndex}> <MotionTabPanel key={activeSubcategory.tabIndex} p="0px" initial={{ x: "100%", opacity: 0 }} animate={{ x: 0, opacity: 1 }} exit={{ x: "-100%", opacity: 0 }} transition={{ duration: 0.3, ease: "easeInOut" }} > <Box overflowX="auto" whiteSpace="nowrap" p="8px" gap="8px" css={{ "&::-webkit-scrollbar": { display: "none" } }} > {categories[activeSubcategory.tabIndex].subcategories.map((subcategory, subIndex) => ( <Button key={subIndex} borderRadius="full" bg={ activeSubcategory.subIndex === subIndex ? "brand.primary" : "mono.backGround" } color={ activeSubcategory.subIndex === subIndex ? "white" : "mono.primary" } size="md" mr="8px" px="18px" py="8px" onClick={() => handleSubcategoryClick(activeSubcategory.tabIndex, subIndex)} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} > {subcategory} </Button> ))} </Box> </MotionTabPanel> </TabPanels> </AnimatePresence> {/* 内容展示区域 */} <Box mt={4} overflowY="auto" maxHeight="400px" css={{ "&::-webkit-scrollbar": { width: "6px" }, "&::-webkit-scrollbar-thumb": { bg: "mono.divider", borderRadius: "3px" } }}> {contentData[activeSubcategory.tabIndex].map((content, subIndex) => ( <div key={subIndex} ref={el => contentRefs.current[activeSubcategory.tabIndex * 10 + subIndex] = el} p={4} mb={2} borderRadius="md" bg={activeSubcategory.subIndex === subIndex ? "mono.light" : "transparent"} > <h3 style={{ marginBottom: '8px', fontWeight: 'bold' }}> {categories[activeSubcategory.tabIndex].subcategories[subIndex]} </h3> <p>{content}</p> </div> ))} </Box> </Tabs> ); };
核心实现细节
子分类平滑过渡
- 用
AnimatePresence包裹TabPanels,配合motion(TabPanel)实现切换动画 - 配置初始从右侧划入、退出向左划出的位移+透明度动画,过渡时长0.3秒
- 用
内容滚动聚焦
- 通过
useRef存储每个内容项的DOM节点 - 点击子分类后调用
scrollIntoView,实现平滑滚动到目标内容
- 通过
交互体验优化
- 为主Tab和子分类按钮添加hover、tap缩放动画
- 选中的子分类内容添加背景高亮,增强视觉识别
内容的提问来源于stack exchange,提问作者Shiba Y.
相关产品推荐
相关产品推荐

