Framer Motion动画实战:用Next.js打造炫酷登录页(附完整代码)
在当今追求极致用户体验的前端开发领域,动画已不再是锦上添花的装饰,而是提升用户参与度和品牌专业度的关键要素。Framer Motion作为React生态中最强大的动画库之一,以其声明式API和流畅的性能表现,成为众多Next.js开发者的首选工具。本文将带您从零开始,构建一个具有专业水准的动画登录页面,涵盖页面加载、表单交互和状态过渡等核心场景,每个环节都配有可直接复用的代码示例。
1. 项目初始化与基础配置
1.1 创建Next.js项目
我们首先使用最新版本的Next.js初始化项目,推荐选择TypeScript模板以获得更好的开发体验:
npx create-next-app@latest motion-login --typescript
cd motion-login
安装Framer Motion核心库及其常用辅助工具:
npm install framer-motion @types/framer-motion
1.2 基础动画组件封装
创建一个可复用的动画组件能显著提升开发效率。在components/AnimatedBox.tsx中:
import { motion, Variants } from 'framer-motion'
const fadeInUp: Variants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
}
export function AnimatedBox({ children, delay = 0 }) {
return (
<motion.div
initial="hidden"
animate="visible"
variants={fadeInUp}
transition={{ duration: 0.6, delay }}
>
{children}
</motion.div>
)
}
这个组件实现了常见的淡入上移动画效果,通过delay参数可以控制动画的延迟时间。
2. 登录页骨架与入场动画
2.1 页面布局结构
在pages/index.tsx中构建基础布局:
import { AnimatedBox } from '../components/AnimatedBox'
export default function LoginPage() {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<main className="container mx-auto px-4 py-16">
<AnimatedBox>
<div className="max-w-md mx-auto bg-white rounded-xl shadow-lg overflow-hidden">
{/* 内容将在这里添加 */}
</div>
</AnimatedBox>
</main>
</div>
)
}
2.2 分阶段入场动画
使用staggerChildren实现元素的顺序出现效果:
const containerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1
}
}
}
const itemVariants: Variants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
}
function LoginForm() {
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="p-8"
>
<motion.div variants={itemVariants} className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-800">欢迎回来</h1>
</motion.div>
{/* 表单元素将在这里添加 */}
</motion.div>
)
}
3. 表单交互动画实现
3.1 输入框焦点动画
为输入框添加聚焦和失焦时的动画反馈:
function AnimatedInput({ label, ...props }) {
return (
<motion.div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-1">
{label}
</label>
<motion.div
whileFocus={{
boxShadow: "0 0 0 2px rgba(99, 102, 241, 0.5)",
borderColor: "#6366f1"
}}
className="relative"
>
<input
className="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none"
{...props}
/>
</motion.div>
</motion.div>
)
}
3.2 按钮点击效果
创建具有按压效果的登录按钮:
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="w-full bg-indigo-600 text-white py-3 rounded-lg font-medium"
>
登录
</motion.button>
4. 高级动画技巧应用
4.1 加载状态动画
为表单提交添加加载动画:
const [isLoading, setIsLoading] = useState(false)
<motion.button
animate={isLoading ? "loading" : "idle"}
variants={{
idle: { width: "100%" },
loading: { width: "48px" }
}}
onClick={() => setIsLoading(true)}
className="relative h-12 bg-indigo-600 text-white rounded-lg font-medium overflow-hidden"
>
<motion.span
animate={isLoading ? "hidden" : "visible"}
variants={{
hidden: { opacity: 0 },
visible: { opacity: 1 }
}}
className="absolute inset-0 flex items-center justify-center"
>
登录
</motion.span>
{isLoading && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="absolute inset-0 flex items-center justify-center"
>
<div className="w-6 h-6 border-2 border-white border-t-transparent rounded-full animate-spin" />
</motion.div>
)}
</motion.button>
4.2 错误状态动画
表单验证错误的动画反馈:
const [error, setError] = useState("")
<motion.div
animate={error ? "visible" : "hidden"}
variants={{
visible: { opacity: 1, height: "auto" },
hidden: { opacity: 0, height: 0 }
}}
className="overflow-hidden"
>
<div className="mt-2 text-sm text-red-600 bg-red-50 p-3 rounded-lg">
{error}
</div>
</motion.div>
5. 页面过渡与路由动画
5.1 页面切换过渡
在_app.tsx中添加全局页面过渡效果:
import { AnimatePresence } from 'framer-motion'
function MyApp({ Component, pageProps, router }) {
return (
<AnimatePresence mode="wait">
<motion.div
key={router.route}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
>
<Component {...pageProps} />
</motion.div>
</AnimatePresence>
)
}
5.2 成功登录后的跳转动画
登录成功后的动画过渡:
const [isSuccess, setIsSuccess] = useState(false)
if (isSuccess) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-16"
>
<motion.div
animate={{
scale: [1, 1.1, 1],
rotate: [0, 5, -5, 0]
}}
transition={{ duration: 0.6 }}
className="inline-block mb-6"
>
<CheckCircleIcon className="h-16 w-16 text-green-500" />
</motion.div>
<h2 className="text-2xl font-bold text-gray-800 mb-2">登录成功</h2>
<p className="text-gray-600">正在跳转到仪表盘...</p>
</motion.div>
)
}
6. 性能优化与最佳实践
6.1 动画性能优化技巧
- 优先使用
transform和opacity属性进行动画,它们不会触发重排 - 对于复杂动画,使用
will-change: transform提示浏览器优化 - 避免同时动画过多元素,可以使用
staggerChildren控制节奏
6.2 移动端适配
const isMobile = useMediaQuery("(max-width: 768px)")
<motion.div
animate={{
x: isMobile ? 0 : 100,
transition: { type: "spring", damping: 10 }
}}
/>
7. 完整代码整合
将所有组件整合到完整的登录页面中:
import { useState } from 'react'
import { motion, AnimatePresence, Variants } from 'framer-motion'
import { CheckCircleIcon } from '@heroicons/react/24/outline'
const containerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1
}
}
}
const itemVariants: Variants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
}
export default function LoginPage() {
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState("")
const [isSuccess, setIsSuccess] = useState(false)
const handleSubmit = (e) => {
e.preventDefault()
setIsLoading(true)
// 模拟API调用
setTimeout(() => {
setIsLoading(false)
setIsSuccess(true)
}, 1500)
}
if (isSuccess) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-16"
>
{/* 成功状态动画 */}
</motion.div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<main className="container mx-auto px-4 py-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="max-w-md mx-auto bg-white rounded-xl shadow-lg overflow-hidden"
>
<motion.form
onSubmit={handleSubmit}
variants={containerVariants}
initial="hidden"
animate="visible"
className="p-8"
>
<motion.div variants={itemVariants} className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-800">欢迎回来</h1>
<p className="text-gray-500 mt-2">请登录您的账户</p>
</motion.div>
<motion.div variants={itemVariants}>
<AnimatedInput
label="邮箱地址"
type="email"
placeholder="your@email.com"
required
/>
</motion.div>
<motion.div variants={itemVariants}>
<AnimatedInput
label="密码"
type="password"
placeholder="••••••••"
required
/>
</motion.div>
<motion.div variants={itemVariants} className="mt-6">
<button
type="submit"
className="w-full bg-indigo-600 text-white py-3 rounded-lg font-medium"
disabled={isLoading}
>
{isLoading ? '登录中...' : '登录'}
</button>
</motion.div>
<AnimatePresence>
{error && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="mt-4"
>
<div className="text-sm text-red-600 bg-red-50 p-3 rounded-lg">
{error}
</div>
</motion.div>
)}
</AnimatePresence>
</motion.form>
</motion.div>
</main>
</div>
)
}
&spm=1001.2101.3001.5002&articleId=154526006&d=1&t=3&u=576f01c699d84e7e90f072c5af9c394c)
553

被折叠的 条评论
为什么被折叠?



