前言
介绍
Framer Motion 是一个适用于 React 网页开发的动画库,它可以让开发者轻松地在他们的项目中添加复杂和高性能的动画效果。该库提供了一整套针对 React 组件的动画、过渡和手势处理功能,使得通过声明式的 API 来创建动画变得简单直观。
接下来我将带你使用 Framer Motion 实现一个选中样式平滑过渡的案例。
案例
基本环境
本案例基于 Vite + React + TypeScript + TailwindCSS 搭建。TailwindCSS 不是必须的,我主要是为了不写 CSS 样式,若想实现和本案例相同的效果,可以安装一下 TailwindCSS。
基本页面
基本页面比较简单,主要就是渲染不同的导航链接,当点击时设置选中的导航,在选中的导航内渲染一个 span
用于显示选中样式,App.tsx
:
tsx
import { useState } from "react";
const navs = [
{
name: "Home",
href: "#home",
},
{
name: "About",
href: "#about",
},
{
name: "Contact",
href: "#contact",
},
];
function App() {
const [activeNav, setActiveNav] = useState("#home");
return (
<div className="mx-auto grid h-full max-w-2xl place-items-center space-y-4 py-4">
<div className="space-y-8">
<ul className="flex gap-2">
{navs.map((nav, index) => (
<li key={index} className="relative">
<a
href={nav.href}
className="block rounded-md px-4 py-2 text-sm font-medium"
onClick={() => {
setActiveNav(nav.href);
}}
>
{nav.name}
{nav.href === activeNav && (
<span className="absolute inset-0 -z-10 rounded-full bg-gray-100"></span>
)}
</a>
</li>
))}
</ul>
<ul className="flex flex-col gap-2">
{navs.map((nav, index) => (
<li key={index} className="relative">
<a
href={nav.href}
className="block rounded-md px-4 py-1 text-sm font-medium hover:bg-gray-100"
onClick={() => {
setActiveNav(nav.href);
}}
>
{nav.name}
{nav.href === activeNav && (
<span className="absolute inset-y-0 left-0 w-1 rounded-full bg-gray-200"></span>
)}
</a>
</li>
))}
</ul>
</div>
</div>
);
}
export default App;
navs
数组:定义了导航链接的数据。activeNav
状态:存储当前选中的导航项。- 导航列表:通过
<ul>
元素创建垂直和水平的导航条目。 - 点击事件处理:更新
activeNav
状态以高亮显示当前选中导航。 - 条件渲染:根据
activeNav
状态在选中的导航下显示指示标识。
此时已经完成了案例的基本样式,通过点击发现此时还没有任何动画,选中之间的过渡比较生硬。
npm 安装 Framer Motion:
bash
npm install framer-motion
使用 pnpm:
bash
pnpm add framer-motion
修改 app.tsx
:
tsx
import { motion } from "framer-motion";
<motion.span
className="absolute inset-0 -z-10 rounded-full bg-gray-100"
layoutId="activeNav"
transition={{
type: "spring",
stiffness: 380,
damping: 30,
}}
></motion.span>
<motion.span
className="absolute inset-y-0 left-0 w-1 rounded-full bg-gray-200"
layoutId="activeNav2"
transition={{
type: "spring",
stiffness: 380,
damping: 30,
}}
></motion.span>
引入 framer-motion,然后把设置选中样式的 span
改为 motion.span
,添加 layoutId
和 transition
属性,动画设置为 spring
弹簧动画。
layoutId
用来创建动画以平滑地过渡共享元素的布局变化。
transition
属性用来定义动画的持续时间、延迟、缓动函数(如线性、弹性)、循环次数等特性。
stiffness
这个参数影响弹簧的硬度。一个更高的刚度值意味着弹簧更硬,它会更快地回到它的起始位置,导致动画更快地开始并且拥有一个更小的震荡周期。简言之,刚度越大,弹簧越"紧",动画运动越快。
damping
这个参数影响弹簧动画的阻尼效果,也就是减震效果。一个更高的阻尼值会导致弹簧运动时震荡得更少,从而更快地稳定下来。如果阻尼值设置得很低,动画会在达到静止状态之前在结束位置附近做较多的震荡。
完整代码
tsx
import { useState } from "react";
import { motion } from "framer-motion";
const navs = [
{
name: "Home",
href: "#home",
},
{
name: "About",
href: "#about",
},
{
name: "Contact",
href: "#contact",
},
];
function App() {
const [activeNav, setActiveNav] = useState("#home");
return (
<div className="mx-auto grid h-full max-w-2xl place-items-center space-y-4 py-4">
<div className="space-y-8">
<ul className="flex gap-2">
{navs.map((nav, index) => (
<li key={index} className="relative">
<a
href={nav.href}
className="block rounded-md px-4 py-2 text-sm font-medium"
onClick={() => {
setActiveNav(nav.href);
}}
>
{nav.name}
{nav.href === activeNav && (
<motion.span
className="absolute inset-0 -z-10 rounded-full bg-gray-100"
layoutId="activeNav"
transition={{
type: "spring",
stiffness: 380,
damping: 30,
}}
></motion.span>
)}
</a>
</li>
))}
</ul>
<ul className="flex flex-col gap-2">
{navs.map((nav, index) => (
<li key={index} className="relative">
<a
href={nav.href}
className="block rounded-md px-4 py-1 text-sm font-medium hover:bg-gray-100"
onClick={() => {
setActiveNav(nav.href);
}}
>
{nav.name}
{nav.href === activeNav && (
<motion.span
className="absolute inset-y-0 left-0 w-1 rounded-full bg-gray-200"
layoutId="activeNav2"
transition={{
type: "spring",
stiffness: 380,
damping: 30,
}}
></motion.span>
)}
</a>
</li>
))}
</ul>
</div>
</div>
);
}
export default App;