# 标签页 Tabs > 同一块地方放几份内容,点上面的标签切换,下面一条指示条跟着滑过去。 分类:导航 [全部组件和流程](https://www.dachuiai.com/llms.txt) · [导航选型规则](https://www.dachuiai.com/guides/navigation.md) ## 给 AI:用户把这个组件发给了你 1. 先看用户的项目:框架、页面类型、现在的风格、手机还是电脑为主。 2. 对照下面的「适合 / 不适合」判断合不合适。不合适就直接说,并从[导航选型规则](https://www.dachuiai.com/guides/navigation.md)或[全部组件](https://www.dachuiai.com/llms.txt)里另挑一个。 3. 合适就按「装进项目」装好,示例里的文字和图片换成用户自己的。 4. 告诉用户用了什么、放在哪个文件、以后想改快慢、颜色、大小改哪个参数。 ## 什么时候用 - 适合:同一件东西的不同内容:商品的详情、评价、参数;设置页的几组选项。 - 不适合:要去完全不同的页面(用导航);标签超过 6 个;几份内容要同时对比。 - 适合的网站:电商、SaaS、后台、内容站、官网 - 放在:内容区 - 手机上:好用:标签别超过 4 个,名字要短。 - 性能代价:低 - 常搭配:[面包屑 Breadcrumb](https://www.dachuiai.com/c/breadcrumb.md)、[侧边栏 Sidebar](https://www.dachuiai.com/c/sidebar.md) ## 装进项目 - 写法:React 18 以上 + TypeScript + Tailwind CSS v4,一个文件就能用,不引用别的组件。 - 依赖:`npm i lucide-react`(lucide-react 只有最后的演示示例用到;不留示例的话,删掉对应的 import 就不用装) - 文件:原样存成 `tabs.tsx`(比如 `src/components/tabs.tsx`)。能跑命令就下载[原文](https://www.dachuiai.com/code/tabs.tsx),比手抄准:`curl -fsSL -o src/components/tabs.tsx https://www.dachuiai.com/code/tabs.tsx`(Windows 写 `curl.exe`)。下载完看一眼第一行是 `import`,不是网页。 - 导出:`Tabs` 是可以直接用的部分;最后的 `export default` 是演示示例,只作参考。 - 图片:示例用了本站的 [/tex/s5.jpg](https://www.dachuiai.com/tex/s5.jpg)。换成用户自己的图;只想先看效果,可以从本站同样的路径下载,放进项目的 public 目录。 - 注意:items 每项是 {key, label, content},initial 是一开始选中的 key。键盘左右键切换、Home / End 跳到两头都做好了。 - 保留代码里 `prefers-reduced-motion` 的处理:用户开了「减弱动态效果」时内容照样完整显示。 - Next.js(有 app 文件夹):文件第一行加 `'use client';`。 - 用 JavaScript 不用 TypeScript:Vite 和 Next.js 都能直接用 .tsx 文件;也可以删掉类型标注,改名 .jsx。 - 没有 Tailwind:先装 Tailwind CSS v4(Vite 项目 `npm i tailwindcss @tailwindcss/vite`,vite 配置的 plugins 里加 `tailwindcss()`,入口引入的那个 CSS 文件(一般是 `src/index.css`)第一行写 `@import "tailwindcss";`),或者把类名改写成项目用的样式。 - 已经装了 Tailwind v3:别为了一个组件升级。类名大多通用,少数 v4 写法要换,比如焦点框 `outline-2` 在 v3 里要写成 `outline outline-2`。 - 不是 React(Vue、Svelte、小程序、纯 HTML):照着代码的做法改写,时长、曲线、参数保持一样。Wix、有赞这类建站平台装不进代码,告诉用户去平台自带的动效设置里找。 ## 参数 | 参数 | 默认值 | 作用 | | --- | --- | --- | | `items` | | 要显示的一组内容,格式见代码里的类型 | | `initial` | | 一开始选中的那一项的 key | ## 你可以说 > 商品的详情、评价、参数做成 Tabs 标签页:点标签切换内容,下面一条指示条跟着滑过去。 ## 完整代码 ```tsx import {useId, useLayoutEffect, useRef, useState, type KeyboardEvent, type ReactNode} from 'react'; import {Check, Star} from 'lucide-react'; /* --------------------------------------------------------------- * Tabs 标签页 * 同一块地方放几份内容,点上面的标签来回切换,下面一条指示条跟着滑过去。 * 切换的是同一件东西的不同内容(详情、评价、参数)用标签页; * 要去一个完全不同的页面,就该用导航。 * 键盘:← → 切换,Home / End 跳到两头。 * ------------------------------------------------------------- */ export type TabItem = {key: string; label: ReactNode; content: ReactNode}; export function Tabs({items, initial}: {items: TabItem[]; initial?: string}) { const uid = useId(); const [active, setActive] = useState(initial ?? items[0].key); const refs = useRef<(HTMLButtonElement | null)[]>([]); const [bar, setBar] = useState({x: 0, w: 0}); const i = items.findIndex((t) => t.key === active); // 指示条只用 transform:平移到标签位置,再横向缩放到标签宽度 useLayoutEffect(() => { const el = refs.current[i]; if (el) setBar({x: el.offsetLeft, w: el.offsetWidth}); }, [i]); const onKey = (e: KeyboardEvent) => { const n = items.length; const to = {ArrowRight: (i + 1) % n, ArrowLeft: (i - 1 + n) % n, Home: 0, End: n - 1}[e.key]; if (to == null) return; e.preventDefault(); setActive(items[to].key); refs.current[to]?.focus(); }; return (
{items.map((t, k) => ( ))}
{items.map((t, k) => ( ))}
); } /* ---------------- 示例:商品详情页 ---------------- */ const REVIEWS = [['小林', '风大的晚上也很稳,三分钟就搭好了。', 5], ['Mia', '比想象中轻,背着走了 8 公里没压力。', 5], ['老周', '前厅再大一点就完美了。', 4]] as const; const SPECS = [['重量', '1.6 kg'], ['展开尺寸', '210 × 130 cm'], ['适用人数', '2 人'], ['防水指数', '3000 mm'], ['适用季节', '三季']]; export default function TabsDemo() { return (

轻量双人帐篷

4.9 · 128 条评价
¥899
{['三分钟快速搭建', '双门双前厅', '防风等级 6 级', '收纳后只有水壶大小'].map((f) => (
  • {f}
  • ))} )}, {key: 'reviews', label: <>评价 128, content: (
      {REVIEWS.map(([n, t, s]) => (
    • {n}{Array.from({length: s}, (_, i) => )}

      {t}

    • ))}
    )}, {key: 'specs', label: '参数', content: (
    {SPECS.map(([k, v]) => (
    {k}
    {v}
    ))}
    )}, ]} />
    ); } ```