本文基于对 LobsterAI 官网的逆向分析,带你从零开始实现专业级的前端动效体验

前言
最近研究了不少 AI 产品官网,LobsterAI 的交互体验给我留下了深刻印象。特别是模型选择区域的动效设计,既流畅又不喧宾夺主。今天我们就来深扒一下这类动效的实现方案。
一、动效分析
1.1 观察到的核心动效
从官网的模型选择区域,我们可以观察到以下动效特征:
- 模型卡片的悬停效果 - 鼠标移入时的轻微上浮 + 阴影加深
- 选中状态的过渡 - 选中模型时的平滑高亮切换
- 图标/徽标的入场动画 - 页面加载时的 staggered fade-in
- 滚动视差 - 不同元素以不同速度滚动
1.2 动效参数拆解
通过观察,我推测出以下关键参数:
css
/* 悬停动画 */
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
/* 选中状态切换 */
transition: background-color 0.2s ease,
border-color 0.2s ease;
/* 入场动画 */
animation: fadeInUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
二、技术选型
2.1 为什么不用动画库?
虽然 Framer Motion、GSAP 等库很强大,但对于这种轻量级动效,原生 CSS + 少量 JS 是更好的选择:
- 性能更优 - 没有运行时开销
- 包体积更小 - 少引入几十 KB 的依赖
- 可控性更强 - 完全掌握每一帧的表现
2.2 核心技术栈
diff
- CSS Transitions - 状态过渡
- CSS Animations - 入场动画
- Intersection Observer - 滚动触发
- requestAnimationFrame - 复杂动画帧控制(可选)
三、实战实现
3.1 模型卡片悬停效果
vue
<template>
<div class="model-card" :class="{ selected: isSelected }" @mouseenter="handleEnter" @mouseleave="handleLeave">
<div class="model-icon">
<img :src="model.icon" :alt="model.name" />
</div>
<span class="model-name">{{ model.name }}</span>
</div>
</template>
<script setup>
defineProps({
model: Object,
isSelected: Boolean
})
const handleEnter = (e) => {
e.currentTarget.style.transform = 'translateY(-4px)'
e.currentTarget.style.boxShadow = '0 12px 24px rgba(0, 0, 0, 0.1)'
}
const handleLeave = (e) => {
e.currentTarget.style.transform = 'translateY(0)'
e.currentTarget.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.05)'
}
</script>
<style scoped>
.model-card {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 8px;
background: #fff;
border: 1px solid #e5e7eb;
cursor: pointer;
/* 核心过渡效果 */
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1),
background-color 0.2s ease,
border-color 0.2s ease;
/* 初始阴影 */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.model-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08);
}
.model-card.selected {
background-color: #f0f9ff;
border-color: #0ea5e9;
}
.model-icon {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.model-icon img {
width: 100%;
height: 100%;
object-fit: contain;
}
</style>
3.2 交错入场动画(Staggered Animation)
这是提升质感的关键!让元素按顺序依次出现:
vue
<template>
<div class="model-list">
<div
v-for="(model, index) in models"
:key="model.id"
class="model-item"
:style="{ animationDelay: `${index * 0.1}s` }"
>
<!-- 模型内容 -->
</div>
</div>
</template>
<style scoped>
.model-item {
opacity: 0;
transform: translateY(20px);
animation: fadeInUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
3.3 滚动触发动画(Intersection Observer)
让动画在元素进入视口时才触发:
js
// composables/useScrollAnimation.js
export function useScrollAnimation() {
const observeElements = (selector, options = {}) => {
const {
threshold = 0.1,
rootMargin = '0px',
once = true
} = options
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-in')
if (once) {
observer.unobserve(entry.target)
}
}
})
}, { threshold, rootMargin })
document.querySelectorAll(selector).forEach(el => {
el.classList.add('animate-ready')
observer.observe(el)
})
return () => observer.disconnect()
}
return { observeElements }
}
css
/* 配合的 CSS */
.animate-ready {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1),
transform 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
.animate-ready.animate-in {
opacity: 1;
transform: translateY(0);
}
3.4 徽标闪烁效果(可选)
如果模型有"新"、"热"等徽标,可以添加微妙的闪烁:
css
.model-badge {
position: absolute;
top: -4px;
right: -4px;
padding: 2px 6px;
font-size: 10px;
border-radius: 4px;
background: linear-gradient(135deg, #ff6b6b, #ee5a5a);
color: white;
animation: badgePulse 2s ease-in-out infinite;
}
@keyframes badgePulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.9;
}
}
四、性能优化
4.1 使用 transform 而非 top/left
❌ 错误示范:
css
.card:hover {
top: -4px; /* 触发重排 */
}
✅ 正确做法:
css
.card:hover {
transform: translateY(-4px); /* 仅触发合成 */
}
4.2 将动画属性提升到独立层
css
.animated-element {
will-change: transform, opacity;
/* 注意:不要滥用,仅在真正需要时使用 */
}
4.3 减少强制同步布局
js
// 避免这样
element.style.transform = 'translateY(-4px)'
console.log(element.offsetHeight) // 强制重排
element.style.opacity = '1'
// ✅ 使用 CSS 类
element.classList.add('hover-state')
五、进阶技巧
5.1 使用 CSS 变量实现主题动画
css
:root {
--transition-duration: 0.3s;
--easing-function: cubic-bezier(0.4, 0, 0.2, 1);
--hover-translate-y: -4px;
}
.model-card {
transition: transform var(--transition-duration) var(--easing-function),
box-shadow var(--transition-duration) var(--easing-function);
}
.model-card:hover {
transform: translateY(var(--hover-translate-y));
}
5.2 响应式动效
css
/* 移动端减少动画幅度 */
@media (max-width: 768px) {
.model-card:hover {
transform: translateY(-2px); /* 减小位移 */
}
.model-item {
animation-duration: 0.4s; /* 加快动画 */
}
}
/* 尊重用户的减少动画偏好 */
@media (prefers-reduced-motion: reduce) {
.model-card,
.model-item {
transition: none;
animation: none;
}
}
5.3 使用 Web Animations API 处理复杂场景
js
// 当 CSS 不够用时
const animateModelSelect = (element) => {
element.animate([
{ transform: 'scale(1)', backgroundColor: '#fff' },
{ transform: 'scale(0.95)', backgroundColor: '#f0f9ff' },
{ transform: 'scale(1)', backgroundColor: '#f0f9ff' }
], {
duration: 300,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)'
})
}
六、完整示例代码
这里提供一个完整的 Vue 3 组件示例:
vue
<template>
<div class="model-selector">
<div class="model-list">
<ModelCard
v-for="(model, index) in models"
:key="model.id"
:model="model"
:is-selected="selectedModel === model.id"
:animation-delay="index * 100"
@select="handleSelect"
/>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import ModelCard from './ModelCard.vue'
const models = ref([
{ id: 'qwen-max', name: 'Qwen3.7-Max', icon: '/icons/qwen.svg' },
{ id: 'qwen-plus', name: 'Qwen3.7-Plus', icon: '/icons/qwen.svg' },
{ id: 'kimi-code', name: 'Kimi-K2.7-Code', icon: '/icons/kimi.svg' },
// ... 更多模型
])
const selectedModel = ref('qwen-plus')
const handleSelect = (modelId) => {
selectedModel.value = modelId
}
onMounted(() => {
// 滚动动画监听
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible')
}
})
}, { threshold: 0.1 })
document.querySelectorAll('.model-card').forEach(el => observer.observe(el))
})
</script>
<style scoped>
.model-selector {
padding: 24px;
background: #f9fafb;
}
.model-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
max-width: 1200px;
margin: 0 auto;
}
</style>
七、调试工具推荐
- Chrome DevTools Animations 面板 - 可视化查看所有动画
- Performance 面板 - 检测掉帧和性能瓶颈
- cubic-bezier.com - 可视化调整缓动曲线
- easings.net - 预设缓动函数参考
八、总结
实现专业级前端动效的关键:
- 克制 - 动效是为了增强体验,不是炫技
- 流畅 - 使用 transform 和 opacity,保持 60fps
- 一致 - 统一使用相同的缓动函数和时长
- 可访问 - 尊重
prefers-reduced-motion - 渐进增强 - 核心功能不依赖动画
希望这篇文章能帮你打造出同样出色的前端动效体验!
参考资料
本文完