Framer motion的核心API是motion的组件。每个HTML和SVG标签都有对应的motion组件。
他们渲染的结果与对应的原生组件完全一致,并在其之上增加了一些动画和手势相关的props。
比如:
<motion.div />
<motion.span />
<motion.h1 />
<motion.svg />
使用与平常的插件相差无几。
npm install framer-motion
当然,这里还是有一点不一样的,如果直接在页面中使用是会报错的。因此,需要暴露出 webpack 配置。执行 npm run eject
在 config / webpack.config.js 中,添加这行代码
{
test: /\.mjs$/,
include: /node_modules/,
type: 'javascript/auto',
},
重新启动项目即可。
官网 API 文档地址👉:https://www.framer.com/docs/
mount
动画
实现一个组件 mount 时的下降显现效果,需要使用 motion 组件的 initial 和 animate 属性。
-
initial 定义组件的初始状态
-
animate定义组件mount时的动画效果。如果其值与initial不同,则会产生过渡的动画效果
import React from 'react'
import { motion } from 'framer-motion'
function App() {
return (
<motion.div
style={{
width: '100px',
height: '100px',
backgroundColor: '#5b1b70'
}}
initial={{ opacity: 0, y: -50 }}
animate={{ opacity: 1, y: 0 }}
>
Hello World!
</motion.div>
)
}
export default App
Framer Motion
检测到 initial
与 animate
中有 value
不同的 key
,对这些 key
执行过渡效果。
例子中 y 轴方向距离会从 -50 变为 0,透明度从 0 变为 1。
unmount
动画
实现 unmount
动画效果,需要将组件包裹在 <AnimatePresence/>
内。
需要指定 exit
属性,当组件 unmount
时,会执行从 animate
到 exit
的动画效果。
import React from 'react'
import { motion, AnimatePresence } from 'framer-motion'
function App() {
return (
<AnimatePresence>
<motion.div
style={{
width: '100px',
height: '100px',
backgroundColor: '#5b1b70'
}}
exit={{ opacity: 0, y: -50 }}
initial={{ opacity: 0, y: -50 }}
animate={{ opacity: 1, y: 0 }}
>
Hello World!
</motion.div>
</AnimatePresence>
)
}
export default App
可以实现如下的动画效果
import "./index.css";
import * as React from "react";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { initialTabs as tabs } from "./ingredients";
export default function App() {
const [selectedTab, setSelectedTab] = useState(tabs[0]);
return (
<div className="window">
<nav>
<ul>
{tabs.map((item) => (
<li
key={item.label}
className={item === selectedTab ? "selected" : ""}
onClick={() => setSelectedTab(item)}
>
{`${item.icon} ${item.label}`}
{item === selectedTab ? (
<motion.div className="underline" layoutId="underline" />
) : null}
</li>
))}
</ul>
</nav>
<main>
<AnimatePresence exitBeforeEnter>
<motion.div
key={selectedTab ? selectedTab.label : "empty"}
animate={{ opacity: 1, y: 0 }}
initial={{ opacity: 0, y: 20 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.15 }}
>
{selectedTab ? selectedTab.icon : "😋"}
</motion.div>
</AnimatePresence>
</main>
</div>
);
}
index.css
body {
--accent: #8855ff;
width: 100vw;
height: 100vh;
background: var(--accent);
overflow: hidden;
padding: 0;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
}
.window {
width: 480px;
height: 360px;
border-radius: 10px;
background: white;
overflow: hidden;
box-shadow: 0 1px 1px hsl(0deg 0% 0% / 0.075),
0 2px 2px hsl(0deg 0% 0% / 0.075), 0 4px 4px hsl(0deg 0% 0% / 0.075),
0 8px 8px hsl(0deg 0% 0% / 0.075), 0 16px 16px hsl(0deg 0% 0% / 0.075);
display: flex;
flex-direction: column;
}
nav {
background: #fdfdfd;
padding: 5px 5px 0;
border-radius: 10px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-bottom: 1px solid #eeeeee;
height: 44px;
}
.tabs {
flex-grow: 1;
display: flex;
justify-content: flex-start;
align-items: flex-end;
flex-wrap: nowrap;
width: 100%;
}
main {
display: flex;
justify-content: center;
align-items: center;
font-size: 128px;
flex-grow: 1;
user-select: none;
}
ul,
li {
list-style: none;
padding: 0;
margin: 0;
font-family: "Poppins", sans-serif;
font-weight: 500;
font-size: 14px;
}
ul {
display: flex;
width: 100%;
}
li {
border-radius: 5px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
width: 100%;
padding: 10px 15px;
position: relative;
background: white;
cursor: pointer;
height: 24px;
display: flex;
justify-content: space-between;
align-items: center;
flex: 1;
min-width: 0;
position: relative;
user-select: none;
}
.underline {
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 1px;
background: var(--accent);
}
li.selected {
background: #eee;
}
li button {
width: 20px;
height: 20px;
border: 0;
background: #fff;
border-radius: 3px;
display: flex;
justify-content: center;
align-items: center;
stroke: #000;
margin-left: 10px;
cursor: pointer;
flex-shrink: 0;
}
.background {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 300px;
background: #fff;
}
.add-item {
width: 30px;
height: 30px;
background: #eee;
border-radius: 50%;
border: 0;
cursor: pointer;
align-self: center;
}
.add-item:disabled {
opacity: 0.4;
cursor: default;
pointer-events: none;
}
ingredients.js
export const allIngredients = [
{ icon: "🍅", label: "Tomato" },
{ icon: "🥬", label: "Lettuce" },
{ icon: "🧀", label: "Cheese" },
{ icon: "🥕", label: "Carrot" },
{ icon: "🍌", label: "Banana" },
{ icon: "🫐", label: "Blueberries" },
{ icon: "🥂", label: "Champers?" }
];
const [tomato, lettuce, cheese] = allIngredients;
export const initialTabs = [tomato, lettuce, cheese];
export function getNextIngredient(
ingredients
) {
const existing = new Set(ingredients);
return allIngredients.find((ingredient) => !existing.has(ingredient));
}
参考文章:
https://juejin.cn/post/6934657845094776845
https://juejin.cn/post/6945015156933918734
https://juejin.cn/post/6907054067420233742