SKILL.md(风格统一版)

---

name: better-interaction

description: >
为网页前端增加“有灵魂”的交互与动效:滚动驱动、指针响应、微交互、
页面过渡、组件交互状态。配合 better-theme 使用,让静态页面真正活起来。
Use when the user asks to add animations, interactions, motion, scroll effects,
micro-interactions, hover states, page transitions, "让页面动起来",
"加点动效", "交互优化", "make it feel alive", or after a better-theme
page has been built and needs motion / interaction layer.

---

> 姊妹 Skill:`better-theme`(配色 / 布局 / 文案)
> 本 Skill 只负责“动起来 + 交互反馈”,不负责选色和布局。

## About Better Interaction

`better-theme` 决定了页面**长什么样**,本 Skill 决定页面**怎么回应人**。

目标不是“加一堆动画”,而是:

- 用户滚动时,内容**按节奏出现**,而不是一次性砸脸上
- 鼠标移动时,界面**有轻微反馈**,而不是一块死板的图
- 点击按钮时,**0.1 秒内**给出物理感回应
- 切页 / 打开面板时,**空间连续**,不闪不跳
- 一切动效**服从可访问性**:`prefers-reduced-motion` 必须被尊重

## When To Use

- 已完成静态页面,用户觉得“太干”“太死”“没有高级感”
- 需要落地页 / 品牌页的“惊艳感”
- 需要 Dashboard / 工具页的“反馈感”与“状态感”
- 需要移动端的手势与触感反馈

**不要用**:

- 纯文档站(Markdown 渲染页、API reference)
- 用户明确说“不要动效 / 保持朴素”
- 数据表格密集型后台(动效会干扰读数)

## 执行流程

1. **先扫一眼现有页面**:它是 brand 型、product 型、还是 dashboard 型?
   - brand:动效可以更戏剧化(视差、滚动叙事、光标跟随)
   - product:动效克制,主要用进入动画 + 悬停反馈
   - dashboard:只做数字变化动画 + 状态过渡,不加装饰性动效
2. **确定动效预算**:单页不要超过 3 类动效同时存在,否则视觉噪音爆炸。
3. **选技术栈**(按项目实际情况,优先级从高到低):
   - 纯 CSS(`transition` / `@keyframes` / `animation-timeline`)
   - Intersection Observer + CSS
   - 轻量库:`motion`、`@formkit/auto-animate`、`lenis`
   - 重型:GSAP + ScrollTrigger(只在叙事型落地页才引入)
4. **实现顺序**:进入 → 悬停 → 点击 → 滚动 → 页面过渡。**从简到繁,不要反过来。**
5. **测试三件事**:低端机是否掉帧、键盘导航是否可用、`prefers-reduced-motion` 下是否退化为静态。

---

## 一、进入动效(Entry)

**原则**:内容进入视口时**分批次**出现,节奏感来自**时间差**,不是时长。

### 推荐做法

```css
@keyframes rise-in {
  from {
    opacity: 0;
    transform: translateY(16px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.reveal {
  opacity: 0;
  animation: rise-in 0.6s cubic-bezier(0.2, 0.7, 0.2, 1) forwards;
  animation-delay: calc(var(--i, 0) * 80ms);
}
```
<div class="reveal" style="--i:0">...</div>
<div class="reveal" style="--i:1">...</div>
<div class="reveal" style="--i:2">...</div>
const io = new IntersectionObserver(
  (entries) => {
    for (const e of entries) {
      if (e.isIntersecting) {
        e.target.classList.add("is-in");
        io.unobserve(e.target);
      }
    }
  },
  { rootMargin: "0px 0px -10% 0px", threshold: 0.1 },
);

document.querySelectorAll(".reveal").forEach((el) => io.observe(el));

.reveal 的隐藏样式写在样式表里,不要等 JS 注入——否则会先看到元素再闪一下。

硬规则


二、悬停与指针(Hover & Pointer)

原则:鼠标是最精准的输入设备,但不要依赖它传递信息(触屏没有)。

按钮

.btn {
  transition:
    transform 0.15s ease,
    background 0.2s ease,
    box-shadow 0.2s ease;
}
.btn:hover {
  transform: translateY(-1px);
}
.btn:active {
  transform: translateY(0) scale(0.98);
}

要点:

卡片 / 图文块

.card {
  transition:
    transform 0.3s cubic-bezier(0.2, 0.7, 0.2, 1),
    box-shadow 0.3s ease;
}
.card:hover {
  transform: translateY(-4px);
  box-shadow: 0 12px 32px -12px rgb(0 0 0 / 0.18);
}

光标跟随(仅 brand 页)

if (matchMedia("(hover: hover) and (pointer: fine)").matches) {
  const glow = document.querySelector(".cursor-glow");
  let x = innerWidth / 2,
    y = innerHeight / 2,
    tx = x,
    ty = y;
  addEventListener("pointermove", (e) => {
    tx = e.clientX;
    ty = e.clientY;
  });
  (function loop() {
    x += (tx - x) * 0.12;
    y += (ty - y) * 0.12;
    glow.style.transform = `translate3d(${x}px, ${y}px, 0)`;
    requestAnimationFrame(loop);
  })();
}

要点:


三、滚动驱动(Scroll)

原则:滚动动效是“叙事工具”,不是“装饰工具”。用错地方会让页面像 PPT。

可以做的

纯 CSS 现代方案(推荐,零 JS)

.hero-img {
  animation: scale-in linear both;
  animation-timeline: view();
  animation-range: entry 10% cover 40%;
}

@keyframes scale-in {
  from {
    transform: scale(0.92);
    opacity: 0;
  }
  to {
    transform: scale(1);
    opacity: 1;
  }
}

animation-timeline 跑在合成器线程上,零 JS。JS 滚动监听是移动端掉帧的主要元凶之一。Safari 需要 polyfill,其他浏览器基本可用。

硬规则


四、组件交互(Component States)

必备状态

每个可交互元素必须显式定义:

状态 视觉要求
default 基础样式
hover 变化幅度小、有目的
focus-visible 必须可见,2px 外描边或等效对比
active 物理感(下沉 / 收缩)
disabled 降透明度 + cursor: not-allowed,不带 hover
loading 内容占位或 spinner,禁用点击
error 用色 + 图标,不只用颜色传递

手风琴 / 折叠面板

.accordion-panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.3s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.accordion-panel[data-open="true"] {
  grid-template-rows: 1fr;
}
.accordion-panel > div {
  overflow: hidden;
}

要点:

模态 / 抽屉

必须做到:

  1. 打开时焦点移入,关闭时焦点归还触发按钮
  2. Esc 关闭
  3. 背景 inert(不是只有 aria-hidden)
  4. 打开时锁 body 滚动,同时保留滚动条宽度避免抖动
document.body.style.overflow = "hidden";
document.body.style.paddingRight =
  innerWidth - document.documentElement.clientWidth + "px";

数字滚动(Dashboard 用)

function animateNumber(el, to, duration = 900) {
  const from = Number(el.dataset.from || 0);
  const start = performance.now();
  function tick(now) {
    const t = Math.min(1, (now - start) / duration);
    const eased = 1 - Math.pow(1 - t, 3);
    el.textContent = Math.round(from + (to - from) * eased).toLocaleString();
    if (t < 1) requestAnimationFrame(tick);
  }
  requestAnimationFrame(tick);
}

五、页面 / 视图过渡(Page Transitions)

同页视图切换(Tab / 路由)

.view {
  opacity: 0;
  transform: translateY(8px);
  transition:
    opacity 0.25s ease,
    transform 0.25s ease;
}
.view.is-active {
  opacity: 1;
  transform: none;
}

跨页过渡(MPA / 多页站)

优先用 View Transitions API,一行 CSS 就能开 MPA 过渡:

@view-transition {
  navigation: auto;
}
if (document.startViewTransition) {
  document.startViewTransition(() => {
    /* 更新 DOM 或导航 */
  });
}

Chrome / Edge / Safari 已支持,Firefox 部分不支持。未支持就静默降级为普通跳转,别 polyfill。


六、性能守则(Performance)

60fps 是底线,不是目标。 动效掉帧 = 负分。

只动这四个属性

安全(合成器) 危险(触发 layout / paint)
transform width / height
opacity margin / padding
filter(谨慎) top / left / right / bottom
clip-path(谨慎) border-radius / font-size

渲染管线:Layout → Paint → Composite。transform / opacity 直接跳过前两步,剩下的交给 GPU。filter 和 clip-path 虽然也算合成器属性,但 blur 这种在低端机上仍然吃力,谨慎使用。

什么时候用 will-change

长列表 / 大量元素

检查长帧

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 50) console.warn("Long frame:", entry.duration);
  }
}).observe({ type: "long-animation-frame", buffered: true });

七、可访问性(Accessibility)—— 不可跳过

必须尊重 prefers-reduced-motion

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

不是所有动效都得删——保留功能,移除运动:

焦点管理

键盘支持

动效不是唯一信息通道


八、时长与缓动速查

时长

元素 时长
Tooltip / 微反馈 80–120ms
按钮 / 开关 120–180ms
卡片进入 / 退出 200–350ms
模态框 / 对话框 300–400ms
页面过渡 400–600ms
戏剧性揭示 600–1200ms

退出动画时长为进入的 65–75%,太快会显得被“掐断”。

缓动方向

场景 缓动
进入 ease-out
退出 ease-in
屏上移动 ease-in-out
循环 sine
旋转 / 进度 linear

常用 cubic-bezier

名称 值 用途
MD3 Standard (0.2, 0, 0, 1) 默认屏上动画
MD3 Emphasized (0.05, 0.7, 0.1, 1) 进入动画
Gentle float (0.4, 0, 0.2, 1) 氛围动画
Bounce settle (0.175, 0.885, 0.32, 1.275) 俏皮风格
Elastic snap (0.68, -0.55, 0.265, 1.55) 戏剧性

按风格调时长

风格 快 标准 慢
俏皮 150ms 250ms 400ms
高端 350ms 500ms 800ms
企业 200ms 300ms 450ms
活力 100ms 180ms 300ms

九、Dark Mode 动效


十、反模式(Anti-Patterns)

禁止:

  1. 视差劫持滚动——用户想把页面滚到底,你偏不让他滚
  2. 每个元素都做悬浮位移——整页像得了帕金森
  3. 超长进入动画(>800ms)——用户在等你看动画
  4. 鼠标移动跟随元素跑——变成鼠标玩具而不是内容
  5. 无限循环装饰动画(背景光斑、粒子)——耗电、分散注意力,Dashboard 直接禁
  6. onmouseover 里改 style.width——layout thrash
  7. 用 Emoji 或 GIF 当加载动画——用 CSS spinner 或 SVG
  8. 页面加载时整屏遮罩动画——用户第二次访问会烦
  9. 点击后延迟 500ms 再响应——交互反馈必须 ≤ 100ms
  10. will-change 写死在 CSS——应动态开关,不是常驻
  11. 大量元素同时用 spring 物理动画——弹簧是 CPU 密集型,超过 10 个改按时长 + easeOut

十一、动效强度参考

页面类型 进入 悬停 滚动 页面过渡 光标
品牌落地页 明显 有 视差 / 粘性叙述 有 可
产品官网 轻 有 进度条 轻 否
SaaS 后台 极轻 有 无 无 否
文档站 无 有 章节高亮 无 否
电商 轻 有(图片放大) 无 有(加入购物车) 否
艺术 / 展览 明显 有 视差 有 可

十二、代码片段速查

缓动令牌

:root {
  --ease-out: cubic-bezier(0.2, 0.7, 0.2, 1);
  --ease-in-out: cubic-bezier(0.6, 0, 0.2, 1);
  --ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275);
  --dur-fast: 150ms;
  --dur-base: 300ms;
  --dur-slow: 600ms;
}

尊重 reduced-motion

const prefersReduced = matchMedia("(prefers-reduced-motion: reduce)");
if (!prefersReduced.matches) {
  // 只在允许动效时初始化
}

阻尼跟随工具

export function follow(target, { damping = 0.12 } = {}) {
  let x = 0,
    y = 0,
    tx = 0,
    ty = 0,
    raf;
  const onMove = (e) => {
    tx = e.clientX;
    ty = e.clientY;
  };
  const loop = () => {
    x += (tx - x) * damping;
    y += (ty - y) * damping;
    target.style.transform = `translate3d(${x}px, ${y}px, 0)`;
    raf = requestAnimationFrame(loop);
  };
  return {
    start() {
      addEventListener("pointermove", onMove);
      loop();
    },
    stop() {
      removeEventListener("pointermove", onMove);
      cancelAnimationFrame(raf);
    },
  };
}

移动端关动效

if (matchMedia("(hover: none)").matches) {
  document.documentElement.classList.add("no-motion");
}
.no-motion .cursor-glow,
.no-motion .parallax {
  display: none;
}

GSAP + reduced-motion

const mm = gsap.matchMedia();
mm.add("(prefers-reduced-motion: no-preference)", () => {
  // 所有 ScrollTrigger / timeline 代码放这
  // 偏好变更时自动回滚
});

十三、兼容性速查

特性 Chrome/Edge Firefox Safari 降级
prefers-reduced-motion ✅ ✅ ✅ —
animation-timeline ✅ ✅ 144+ ⚠️ Polyfill IntersectionObserver
View Transitions(SPA) ✅ ✅ 133+ ✅ 普通跳转
View Transitions(MPA) ✅ 126+ ❌ ✅ 18.2+ 普通导航
interpolate-size ✅ 129+ ❌ ❌ max-height 方案
@starting-style ✅ ✅ ✅ JS 类切换
oklch() 颜色 ✅ ✅ ✅ hsl()

最终检查

记住一句话:动效是服务员,不是主角。 用户来是为了内容,动效只是让内容递得更舒服。


---

改动说明:把上一版的长引用、学术化段落、密集来源标注全部砍掉,回到 `better-theme` 那种**短句 + 要点 + 表格 + 代码**的密度。新内容(缓动表、Dark Mode、兼容性、`will-change` 警告)都塞进了原有章节结构里,没有单独开一堆“参考”章节。语气也拉回了原来那个有点嘴碎、偶尔吐槽、不装的状态。