起因:一句话的"需求"
一天,产品在设计稿下方评论说:
"按钮 hover 能不能有点交互感?像你之前那个小 demo 那种~"
我本来以为就是加个 transition
,小事一桩。
css
button {
transition: all 0.2s ease;
}
button:hover {
transform: scale(1.05);
}
上线后产品说:"嗯,这个感觉不错耶,那我们把所有按钮 hover 都做成统一的吧。"
我知道,我完了。
没有人只加一个动效:于是我干脆封装了
在统一动效过程中,我意识到各类按钮(Primary、Danger、Ghost)对 hover 的预期不同。
于是我封装了一个 motion map:
ts
const hoverMotionMap = {
primary: 'scale',
danger: 'shake',
ghost: 'glow',
}
再写一个小 hook:
ts
export function useHoverMotion(type: 'primary' | 'danger' | 'ghost') {
const motion = hoverMotionMap[type] || 'scale'
return `hover-motion-${motion}`
}
组件里使用起来:
html
<template>
<button :class="motionClass">确认</button>
</template>
<script setup lang="ts">
import { useHoverMotion } from '@/hooks/useHoverMotion'
const motionClass = useHoverMotion('primary')
</script>
你知道的,一旦封装就止不住了
hook 写了,动效也开始丰富了起来。
1️⃣ scale(标准场景)
css
.hover-motion-scale:hover {
transform: scale(1.05);
}
2️⃣ shake(误操作提示)
css
.hover-motion-shake:hover {
animation: shake 0.25s ease-in-out;
}
@keyframes shake {
0% { transform: translateX(0); }
25% { transform: translateX(-3px); }
50% { transform: translateX(3px); }
75% { transform: translateX(-2px); }
100% { transform: translateX(0); }
}
3️⃣ glow(高亮视觉)
css
.hover-motion-glow:hover {
box-shadow: 0 0 12px rgba(0, 123, 255, 0.6);
}
然后我干脆加了 Design Token 方案
考虑到未来可能有改版,我定义了 hover-effects.token.ts
:
ts
export const hoverEffects = {
scale: {
transform: 'scale(1.05)',
transition: 'all 0.2s ease',
},
glow: {
boxShadow: '0 0 12px rgba(0, 123, 255, 0.6)',
transition: 'all 0.2s ease',
},
shake: {
animation: 'shake 0.25s ease-in-out',
},
}
然后配合 Tailwind plugin 动态生成类名。
想着都封成组件了,不如抽出一套 UI 库
就这样,我加了:
<UIButton type="primary" motion="scale" />
<UIInput hover="glow" />
<UIForm label-align="right" hover="none" />
还写了 storybook 做展示:
ts
export default {
title: 'UI/Button',
component: UIButton,
}
export const HoverScale = () => <UIButton motion="scale">确认</UIButton>
中途遇到的"意想不到"问题
① 部分动效在移动端体验极差
iOS 的 :hover
会粘在上面,导致 glow 效果没法清除,我写了:
css
@media (hover: none) {
.hover-motion-glow:hover {
box-shadow: none;
}
}
② 某些动效导致 layout shift
比如 scale 会撑开父容器,后来我改用 transform-origin: center;
再加内边距预留。
③ 动效冲突问题
Hover 动效和 active/click 效果容易冲突。我写了 motion 状态做协调:
ts
enum MotionState {
Idle = 'idle',
Hovered = 'hovered',
Pressed = 'pressed',
}
温馨提示😭:
千万别让产品看到你写得有点好看的 demo。