标签组件随处可见:在仪表板、设置面板以及产品页面中都能找到它们的身影。但大多数实现方式都显得呆板、缺乏活力,也容易让人遗忘。如果能让这些标签具有生命力该多好呢?比如让它们带有流畅的弹簧动画效果,在鼠标悬停时展现出堆叠卡片样式,并且有一个精美的活动指示器在各个按钮之间滑动切换……
一个基本的标签切换器仅仅能够显示或隐藏内容而已。而一个更优秀的标签切换器则能为用户提供清晰的活动状态提示、流畅的过渡效果,以及一些能够让界面显得生动活泼的动态效果。这就是这个组件的设计理念:它是一个采用Shadcn风格构建的可复用的动画标签系统,同时结合了React、Tailwind CSS和Motion技术。
在这个教程中,你将会 exactly 建造这样一个组件:一个由Shadcn/ui、Framer Motion以及Shadcn Space提供的现成组件共同构建的、具有完整动画效果的标签组件。
完成学习后,你将获得一个可复用的组件,它具备以下特性:
-
带有弹簧动画效果的活动标签指示器
-
鼠标悬停时会出现堆叠卡片样式的效果
-
当活动标签发生变化时,会有一段流畅的进入动画效果
-
使用Shadcn/ui的CSS变量,实现与主题风格完全匹配的样式设计
视频教程:如果你更喜欢通过视觉方式来学习,可以在YouTube上观看完整的教程:
目录
先决条件
在开始之前,请确保您已经掌握了以下内容:
-
React和TypeScript的基础知识
-
Tailwind CSS工具类
-
Shadcn/ui的基础知识(包括组件的安装与主题设置)
您还需要一个已经配置好了以下内容的Next.js或Vite项目:
-
已安装并初始化了Shadcn/ui
-
已安装Framer Motion(也称为motion/react)
您将构建什么
以下是本教程中将会创建的组件架构概览:
AnimatedTabMotion (页面/演示入口点)
└── Tabs (标签栏 + 内容管理模块)
├── Tab buttons (带有弹簧动画效果的激活按钮)
└── FadeInStack (堆叠式、可动画化的内容面板)
主要功能包括:
-
弹簧动画效果 – 这种动画效果是指活跃标签的指示器会沿着弹簧物理规律从一个按钮移动到另一个按钮,而非通过普通的CSS过渡效果实现。这种移动方式会伴随着轻微的弹跳感,从而模拟出真实物体的运动轨迹。
-
堆叠式卡片显示效果
– 非活跃标签面板会被显示在活跃标签的背后,其大小会缩小且颜色会略微变淡,从而营造出层次分明的视觉效果。
-
悬停时的展开效果
– 当用户将鼠标悬停在内容区域上时,堆叠的卡片会垂直展开。
-
新标签页加载时的动画效果
– 当选择新的标签页时,顶部的活跃卡片会先向下移动,然后再回到原来的位置。
通过Shadcn Space CLI安装组件
Shadcn Space是一个收录了已准备好可供实际使用的、与Shadcn/ui兼容的组件的注册库。您可以直接使用Shadcn CLI将这些组件导入到您的项目中,而无需从头开始构建它们。
请查阅他们的入门指南,了解如何将Shadcn CLI与第三方注册库配合使用。
根据您使用的包管理器,运行以下命令之一:
pnpm
pnpm dlx shadcn@latest add @shadcn-space/tabs-01
npm
npx shadcn@latest add @shadcn-space/tabs-01
Yarn
yarn dlx shadcn@latest add @shadcn-space/tabs-01
Bun
bunx --bun shadcn@latest add @shadcn-space/tabs-01
这样,该组件文件就会被导入到您的项目中,并且会自动与您现有的Shadcn/ui主题设置进行关联。之后您可以根据需要对其进行自定义或扩展,而这正是本教程所要教授的内容。
了解组件的结构
在编写任何代码之前,让我们先仔细看看这个组件的整体构成,并将其分解成各个逻辑部分。以下是该组件的完整实现代码:
"use client";
import { useState } from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
type Tab = {
title: string;
value: string;
content?: React.ReactNode;
};
type TabsProps = {
tabs: Tab[];
containerClassName?: string;
activeTabClassName?: string;
tabClassName?: string;
contentClassName?: string;
};
const tabs = [
{
title: "产品",
value: "product",
content: (
产品选项卡
),
},
{title: "服务",
value: "services",
content: (
服务选项卡
),
},
{
title: "测试区",
value: "playground",
content: (
测试区选项卡
),
},
{
title: "内容",
value: "content",
content: (
内容选项卡
),
},
{
title: "随机",
value: "random",
content: (
随机选项卡
),
},
];
const Tabs = ({
tabs,
containerClassName,
activeTabClassName,
tabClassName,
contentClassName,
}: TabsProps) => {
const [activeIdx, setActiveIdx] = useState(0);
const [hovering, setHovering] = useState(false);
const handleSelect = (idx: number) => {
setActiveIdx(idx);
};
const reorderedTabs = [
tabs[activeIdx],
...tabs.filter((_, i) => i !== activeIdx),
];
return (
{tabs.map((tab, idx) => {
const isActive = idx === activeIdx;
return (
);
))}
);
};
type FadeInStackProps = {
className?: string;
tabs: Tab[];
hovering?: boolean;
};
const FadeInStack = ({ className, tabs, hovering }: FadeInStackProps) => {
return (
{tabs.map((tab, idx) => (
{tab.content}
))}
);
};
export default function AnimatedTabMotion() {
return (
);
}
现在,让我们逐一分析这些内容。
步骤1:定义标签页的数据类型
type Tab = {
title: string;
value: string;
content?: React.ReactNode;
};
type TabsProps = {
tabs: Tab[];
containerClassName?: string;
activeTabClassName?: string;
tabClassName?: string;
contentClassName?: string;
};
Tab类型定义了每个标签页项的结构:
-
title——在标签按钮中显示的文本标签。 -
value——用于识别每个标签页的唯一键(同时也作为Framer Motion中的layoutId)。 -
content——一个可选的React.ReactNode,这意味着你可以使用任何JSX代码作为面板内容。
TabsProps类型使得Tabs组件具有很高的可组合性。每个视觉层都可以通过设置className来改变样式,因此你可以在不修改核心逻辑的情况下,独立地重新设计活动标签页、各个标签按钮以及内容区域。
步骤2:构建标签页数据数组
const tabs = [
{
title: “产品”,
value: “product”,
content: (
产品标签页
), }, // ... 其他标签页 };
每个标签页的content都是使用Shadcn/ui中的语义化标记进行样式的,比如bg-muted、text-foreground和border-border。这样设计是有意为之:这些标记会自动适应你的浅色/深色主题设置,而无需额外配置。
你可以用任何实际内容替换这些占位符
步骤3:构建Tags组件(标签栏+状态管理)
const [activeIdx, setActiveIdx] = useState(0);
const [hovering, setHovering] = useState(false);
有两个状态变量控制着整个组件的行为:
-
activeIdx用于记录当前选中的标签页的索引位置。 -
hovering用于判断用户的鼠标指针是否悬停在任何标签按钮上,这个状态会被传递给FadeInStack>组件,从而触发渐显效果。
为实现堆叠效果重新排列标签页顺序
const reorderedTabs = [
tabs[activeIdx],
…tabs.filter((_, i) => i !== activeIdx),
];
这是该架构中最巧妙的设计之一。系统并不会只显示当前活动标签页的内容,而是始终会渲染所有标签页面板——只不过会将活动标签页放在数组的第一个位置。正是这样的设计机制,才使得页面呈现出堆叠卡片的视觉效果:
-
索引0对应的标签页为活动状态,会以全尺寸和最高透明度显示在最上方。
-
索引1、2对应的标签页会堆叠在其下方,显示尺寸和透明度都会降低。
-
索引3及以上的标签页会被隐藏(透明度为0)。
使用“弹簧效果”来渲染标签按钮
>
{isActive && (
>
{tab.title}
这里的关键在于motion_divlayoutId=“clickedbutton”。当一次只有一个元素拥有这个layoutId时,Framer Motion会跟踪它在DOM中的位置;当这个元素从一个按钮上移除并出现在另一个按钮上时,Framer Motion会自动完成这两个位置之间的过渡动画。这样一来,就无需进行任何手动计算,就能实现滑动效果。
过渡动画的配置中使用了bounce: 0.3和duration: 0.6这些参数,因此动画效果显得自然且带有轻微的弹性,而不是机械式的线性滑动。
按钮上设置的transformStyle: “preserve-3d”使得CSS的3D变换效果得以生效,而容器上设置的[perspective:1000px]则进一步营造出了层次感。
步骤4:构建FadeInStack组件
const FadeInStack = ({ className, tabs, hovering }: FadeInStackProps) => {
return (
>
{tab.content}
))}
);
};
让我们来分析一下每个motion.div
scale: 1 - idx * 0.1
当前活跃的卡片后面的每一张卡片,其大小都会减少10%。具体来说:
-
活跃卡片(索引为0):scale: 1.0
-
第二张卡片(索引为1):scale: 0.9
-
第三张卡片(索引为2):scale: 0.8
这种方式能够使堆叠的层之间产生明显的深度层次感。
top: 当鼠标悬停时,值为 idx * -15;否则为 0
当 hovering 的值为 true 时,每张卡片都会向上移动 idx * 15px 的距离。当前激活的卡片不会移动(当 idx 为 15 时,移动距离为 0),而它后面的卡片会分别向左右各移动 -15px、-30px 等距离。这样,在鼠标悬停时就会产生“卡片逐渐展开”的视觉效果。
zIndex: -idx
负数的 z-index 值会使卡片按顺序堆叠:当前激活的卡片位于最上方(z-index 为 0),而后面的卡片则会依次向下排列。
opacity: 如果 idx 小于 3,则值为 1 - idx * 0.1;否则为 0
索引号为 3 及以上的卡片会完全隐藏。前三个卡片的透明度会逐渐降低,分别为 1.0、0.9、0.8。
animate={{ y: 如果 idx 等于 0,则值为 [0, 40, 0];否则为 0 }}
只有当前激活的卡片(索引号为 0 的卡片)会应用这个动画效果。当某个标签页被选中时,reorderedTabs 数组会被重新排序,新的激活卡片会先向下移动 40px,然后再弹回原来的位置。这种动画效果能够快速、直观地提示用户标签页已经发生了变化。
layoutId={tab.value}
每张卡片都有一个与之对应的 layoutId 和 value。当 reorderedTabs 数组被重新计算、各卡片的排列顺序发生变化时,Framer Motion 能够识别出每张卡片的身份,并使它们在新的位置之间平滑地过渡,从而避免出现突兀的跳转效果。
步骤 5:组合页面组件
export default function AnimatedTabMotion() {
return (
);
}
外层容器使用了 [perspective:1000px] 这一属性——这是 Tailwind CSS 中的一个自定义属性,用于设置 CSS 的 perspective 值。正是这个属性让标签页按钮具备了 3D 深度效果。
max-w-5xl 和 mx-auto 这些属性使得该组件在宽屏设备上能够居中显示,而 items-start 则使标签栏左对齐,这种布局方式与大多数实际应用的 UI 规范是一致的。
步骤 6:自定义组件
由于 Tabs 允许为每个视觉层指定不同的类名,因此你可以根据自己的设计需求对这个组件进行完全的重新设计。例如,以下代码示例可以让激活状态的标签页显示得更醒目,并调整布局间距:
<Tabs
tabs={tabs}
containerClassName="gap-1"
tabClassName="text-xs px-3 py-1.5"
activeTabClassName="bg-zinc-900 dark:bg-white"
contentClassName="mt-6"
/>
<您也可以用实际内容替换这些占位符内容面板。下面是一个使用带有真实描述信息的卡片作为示例的例子:>
const tabs = [
{
title: "概述",
value: "overview",
content: (
产品概述</h2>
</p>
>
),
},
// ...
];
实时预览

关键概念总结
以下是该组件中使用的核心Framer Motion技术的概述:
技术名称
功能作用
motion_div元素上的layoutId
用于在DOM中移动共享元素(例如滑动按钮)
每个标签页对应的motion.div元素上的layoutId
在重新排序标签页时,该技术可确保卡片身份不被丢失,从而让Framer Motion能够正确处理位置变化
animate={{ y: [0, 40, 0] }}
用于在标签页切换时实现弹跳效果的关键帧动画
style={{ scale, top, zIndex, opacity }}
内联的响应式样式,用于创建堆叠卡片的视觉效果
transition={{ type: "spring" }}
使用基于物理原理的弹簧曲线来实现动画效果,而非传统的CSS缓动函数
总结
通过本教程,您使用了Shadcn/ui和Framer Motion开发出了一个具有完整动画效果且能适应不同主题的标签页组件。您学会了以下内容:
-
如何使用layoutId来实现带有弹簧效果的滑动指示器
-
如何同时渲染所有标签页面板,并通过重新排序来创建堆叠卡片效果
-
如何利用内联的响应式style属性来实现悬停效果和深度层次感
-
如何使用Framer Motion的关键帧动画来实现触觉上的弹跳效果
-
如何通过覆盖类名来使该组件具有高度可定制性
这种将Shadcn/ui的语义化设计元素与Framer Motion的布局动画相结合的设计模式,不仅适用于标签页组件,还可以应用于轮播图、图片库、通知提示框等多种场景。您可以使用相同的layoutId和堆叠排序技术来实现这些功能。
您可以在Shadcn Space网站上探索这个组件的完整实现以及更多具有动画效果的UI元素。通过CLI命令,您可以非常方便地将这些高质量组件直接应用到您的项目中。
资源
本文的撰写得到了Mihir Koshti(高级全栈开发工程师)的帮助——在LinkedIn上与我联系吧。
Comments are closed.



