关于 apple-design
apple-design 是用于构建流畅 Apple 风格网页界面的设计技能,内容源自 Apple 的 WWDC 设计讲座。它指导如何让动效跟随手势、承接速度并可随时中断。
主要功能
- 01即时交互反馈
- 02指针一比一跟踪
- 03可中断且感知速度的弹簧
- 04动量投射与吸附
- 05空间一致的转场
- 06帧级流畅动效指导
适用场景
- 01设计可拖动的网页界面
- 02构建可中断的抽屉和面板
- 03为手势选择弹簧行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ emilkowalski/ skills --skill apple-design安装后在 Claude Code 中输入/apple-design确认已加载。
详细介绍
Apple Design
初始响应
首次调用此技能且没有提出具体问题时,只回复:
我已准备好帮助你在 Web 上构建流畅的 Apple 风格界面;我的知识来自 Apple WWDC 设计演讲,并已针对 Web 翻译。
在用户提问之前,不要提供任何其他信息。
Apple 如何构建界面,让它不再像一台电脑,而更像是你的延伸。这些知识来自 Apple 的 WWDC 设计演讲——主要是 Designing Fluid Interfaces(WWDC 2018)——并经过提炼,转换为 Web 平台的内容(CSS、Pointer Events、requestAnimationFrame、Motion/Framer Motion 等弹簧库)。
核心要点:当动效从当前屏幕值开始,继承用户的速度,将动量向前投射,并且可以在任意时刻被抓取和反向时,界面就会显得鲜活。 弹簧是实现这一切的工具,因为它们本质上可被中断,并能感知速度。
核心理念
“当我们让界面与思考和移动的方式保持一致时,奇妙的事情就会发生——它不再像一台电脑,而开始像是我们的无缝延伸。”
界面若表现得像物理世界,就会显得流畅:事物会即时响应、连续移动、承载动量、在边界处受到阻力,并且能在运动过程中改变方向。下面的一切都是为了更接近这一点。
Apple 将设计定位为满足人类的四种需求:安全感/可预测性、理解、成就感和愉悦感。 这里的每条规则都服务于其中一种需求。
1. 响应——消除延迟
一旦出现延迟,直接操控的感觉就会“断崖式下跌”。响应能力是一切的基础。
- 在指针按下时响应,而不是等到松开。 按钮按下的瞬间就高亮显示。等到
click/触摸抬起后才提供反馈,会让界面显得迟钝。 - 留意每一处延迟。 检查防抖、人工计时器、过渡等待时间,以及约 300ms 的点击延迟。输入路径上任何非必要的环节都是一种退步。
- 反馈必须在交互过程中持续进行,而不仅仅在结束时出现。 对于拖动、滑块或抽屉,整个过程中都要让 UI 与指针保持 1:1 同步——绝不要等手势完成后才播放动画。
/* Feedback lives on the press, and it's instant */
.button:active {
transform: scale(0.97);
transition: transform 100ms ease-out;
}
2. 直接操控——1:1 跟随
“触控与内容应当同步移动。”
用户拖动某个对象时,对象必须紧贴手指移动,并且保留相对于抓取位置的偏移。在抓取时让对象吸附到中心,会立刻破坏这种错觉。
- 使用 Pointer Events 和
setPointerCapture,让指针离开元素边界后仍能继续跟踪。 - 记录一段简短的速度/位置历史(最近几次
pointermove事件),而不只是记录当前点——松开时需要用到速度。
el.addEventListener('pointerdown', (e) => {
el.setPointerCapture(e.pointerId);
const grabOffset = e.clientY - el.getBoundingClientRect().top; // respect where they grabbed
// ...track position + timestamp history for velocity
});
3. 可中断性 — 最重要的原则
“思考与动作同步发生。”
每个动画都必须随时可中断、可重定向。用户必须能够在元素运动过程中抓住它并立即反向操作,无需等待动画结束。用户再次抓住正在关闭的模态框时,模态框应跟随手指移动,而不是先关闭完毕再重新打开。
- 过渡期间绝不锁定输入。
- 始终从呈现值(当前值)而非目标值开始动画。 中断时,读取元素在屏幕上的实时变换值,并从该值开始新动画。从逻辑值/目标值开始会导致明显跳动。
- 避免将 CSS 过渡和
@keyframes用于任何由手势驱动的效果——它们无法在运动过程中被平滑抓取并反向操作。弹簧默认从当前值开始动画,正好满足中断操作的需要。 - 手势反向时,要融合速度,而不是骤然切断。 在反向时用一个动画替换另一个动画会造成速度不连续,仿佛撞上一堵“砖墙”。能够在重新设定目标时保留速度的弹簧库可以避免这种情况。(这就是 iOS 原生的叠加动画所实现的效果;在 Web 上,应选择能够从当前速度重新设定目标的弹簧库。)
- 将 2D 运动拆分为独立的 X 轴和 Y 轴弹簧。 对二维距离使用单个弹簧,会在 X 轴和 Y 轴速度不同时导致不同步。
4. 行为优先于动画 — 使用弹簧
“把动画看作你与物体之间的对话,而不是由界面预先规定的东西。”
预先编写、时长固定的动画无法响应新的输入。弹簧可以——新输入只需改变目标,运动就能保持连续。凡是用户可以触碰的对象,都应优先使用弹簧。
Apple 特意用两个便于设计师理解的参数替代了物理参数三元组(质量/刚度/阻尼)。请从以下参数入手:
- 阻尼比——控制过冲。
1.0= 临界阻尼,不会反弹,平滑停止。< 1.0= 超过目标并振荡。数值越低,弹性越强。 - 响应时间——数值到达目标的速度,单位为秒。数值越低,响应越快。这不是“时长”——弹簧没有固定时长;停止所需时间由参数决定。
默认值:
- 大多数 UI 动画从 阻尼
1.0(临界阻尼)开始——优雅且不分散注意力。 - 仅当手势本身带有动量时(轻弹、抛出、拖动后释放)才增加反弹(阻尼约为
0.8)。刚淡入的菜单出现过冲会让人觉得不自然;轻弹出去的卡片出现过冲则恰到好处。
Apple 发布版中的具体数值:
| 交互 | 阻尼 | 响应时间 |
|---|---|---|
| 移动/重新定位(如画中画) | 1.0 | 0.4 |
| 旋转 | 0.8 | 0.4 |
| 抽屉/面板 | 0.8 | 0.3 |
Web 映射(Motion / Framer Motion): bounce + duration 弹簧 API 与 Apple 的阻尼比 + 响应时间十分接近。建议采用安全统一的样式:默认在所有地方使用 damping: 1.0 弹簧;仅在由动量驱动的物理交互中使用反弹效果。
import { animate } from 'motion';
// Critically damped default (no overshoot)
animate(el, { y: 0 }, { type: 'spring', bounce: 0, duration: 0.4 });
// Momentum interaction — a little bounce, only because a flick preceded it
animate(el, { y: target }, { type: 'spring', bounce: 0.2, duration: 0.4 });
5. 速度交接 — 拖动与动画之间的衔接
手势结束时,动画必须以与手指完全相同的速度继续运动,这样拖动与动画之间就不会出现明显的衔接痕迹。这一细节最能区分“流畅”与“还行”。
将指针释放时的速度作为弹簧的初始速度传入。有些弹簧 API 需要相对速度——用它除以到目标的剩余距离进行归一化:
relativeVelocity = gestureVelocity / (targetValue − currentValue)
示例:元素位于 y=50,目标位置为 y=150(还差 100px),手指移动速度为 50px/s → 初始弹簧速度 = 50 / 100 = 0.5。Framer Motion / Motion 可直接使用绝对速度(px/s)(velocity 选项),因此通常直接传入原始值。
6. 动量投影——动画到手势的前进方向
“小幅输入,大幅输出。”
不要从释放点吸附到最近的边界。用速度来投影静止位置——就像滚动减速一样——然后吸附到最接近该投影位置的目标。这会让快速轻扫感觉像是把元素甩了出去。
Apple 的精确投影函数(来自 Designing Fluid Interfaces 示例代码):
// decelerationRate ≈ 0.998 for normal scroll feel; 0.99 for snappier
function project(initialVelocity /* px/s */, decelerationRate = 0.998) {
return (initialVelocity / 1000) * decelerationRate / (1 - decelerationRate);
}
const projectedEndpoint = currentPosition + project(releaseVelocity);
const target = nearestSnapPoint(projectedEndpoint); // choose target from the projection
animateSpringTo(target, { velocity: releaseVelocity }); // then hand off velocity (§5)
注意:物理学教材中的 v²/(2·decel) 并非 Apple 实际采用的形式——请使用上面的指数衰减形式。这是优秀底部面板和轮播(Vaul、Embla)的标准行为。
7. 空间一致性——对称路径,锚定原点
“如果某个东西从一个方向消失,我们会期待它从来处出现。”
- 进入和退出沿同一路径。 从右侧滑入的面板必须向右侧关闭。从右侧进入、从底部退出,会让人感到脱节和困惑。
- 将交互锚定到其来源。 菜单、弹出框或面板应从触发它的元素开始——将
transform-origin设置为触发元素,这样按钮与内容之间的空间关系就一目了然。(这与弹出框从触发元素而非中心缩放的原点感知原则相同。) - 在可逆过渡中使用镜像缓动,使离开路径与返回路径相匹配(为两个方向使用反向的三次贝塞尔控制点)。
8. 提示手势的方向
人会根据运动轨迹预测最终状态。中间运动应暗示事物将要前往的方向——控制中心模块会“朝手指方向向上、向外展开”。让过渡帧指向最终结果,而不是盲目地插值到结果。
9. 橡皮筋效果——柔性边界
到达边缘时,应逐渐增加阻力,而不是戛然而止。突然停止会让人觉得“卡住了”;持续的阻力则会让人觉得“仍有响应,只是已经到头了”。随着用户拖动得越来越超出边界,逐步增加阻尼。
// The further past the bound, the less the element follows — real things slow before they stop
function rubberband(overshoot, dimension, constant = 0.55) {
return (overshoot * dimension * constant) / (dimension + constant * Math.abs(overshoot));
}
10. 手势设计细节(“手感”检查清单)
- 轻触: 触摸按下时立即高亮,触摸抬起时确认。为目标周围增加约 10px 的迟滞/触控内边距,并支持通过拖开再拖回取消操作。
- 拖动/轻扫: 在确认方向之前,要求达到一个较小的移动阈值(迟滞,约 10px),然后按 1:1 跟踪。
- 从第一次移动开始,就并行检测所有合理的手势;意图明确后,再果断取消未胜出的手势。避免使用只报告最终状态的识别器(
swipeleft类型的事件)——它们会丢弃反馈所需的连续跟踪信息。 - 尽量减少歧义消除延迟。 双击检测不可避免地会延迟单击;只有在确实支持双击的地方才付出这一代价。
11. 帧级流畅度
流畅度取决于帧中的内容,而不仅仅是帧率。
- 将每帧的位置变化保持在感知阈值以下,以避免频闪。
- 对于极快的运动,轻微的运动模糊/拉伸比清晰锐利的拖影更能体现速度,观感也更好。
requestAnimationFrame是 Web 与显示器同步的时钟(Apple 使用CADisplayLink)。只对适合由合成器处理的属性进行动画——transform和opacity——并在运动即将发生时通过will-change提示浏览器。
12. 材质与纵深——半透明传达层级
Apple 将半透明材质用作悬浮的功能层,在构建结构的同时不抢夺焦点。在 Web 上,可用 backdrop-filter 近似实现。
- 将导航栏/工具栏/面板构建为半透明层(
backdrop-filter: blur()+ 半透明背景),让内容在其下方滚动——不要使用会占据固定区域的不透明栏。 - **材质的厚重感编码层级:**更暗、更厚重的材质用于分隔结构区域(侧边栏);更轻盈的材质用于突出交互元素(按钮)。绝不要将浅色半透明表面叠在另一层浅色半透明表面上——这会导致可读性崩溃。
- **较大的表面应显得更厚:**相比小型标签,使用更强的模糊和更深的阴影。可考虑根据上下文调整阴影——在繁杂/文本内容上方用较重的阴影增强分隔,在纯色背景上方则用较轻的阴影。
- **调暗以聚焦,分离以保持流程。**模态任务应在表面配合调暗遮罩,并将背景压暗/推远。并行的非阻塞面板则使用半透明效果和偏移,不加遮罩,以免打断流程。对于堆叠的面板,逐层调暗并推远每个父层。
- **鲜明效果能让文字在变化的背景上保持可读。**在模糊/半透明表面上,不要使用纯灰色文字——应提高对比度、略微加粗,并稍微增加字间距。将颜色放在纯色层上,而不是半透明前景上。
- **用滚动边缘效果代替硬分隔线。**不要在粘性标题下方添加 1px 边框,而是在内容与悬浮界面相接处使用淡出的轻微模糊/渐变遮罩——仅当悬浮 UI 实际覆盖内容时使用。
- **让材质显现,而不只是淡入。**对于玻璃/模糊表面,在进入/退出时同时为模糊半径和缩放添加动画,让表面看起来像真实材质显现,而不是单纯的透明度渐变。
.toolbar {
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(20px) saturate(180%);
border-top: 1px solid rgba(255, 255, 255, 0.4); /* bright top edge = light catching the material */
}
13. 多模态反馈——动作 + 声音 + 触觉
组合感官反馈的三条规则(摘自 Designing Audio-Haptic Experiences):
- 因果性——必须让人一眼看出是什么触发了反馈。在实际因果事件发生时触发(开关切换、项目吸附归位),并让反馈特征与动作的物理属性相匹配。
- 协调性——视觉、声音和触觉必须在同一帧触发。它们之间的延迟会破坏错觉。不要让 CSS 过渡落后于音频/触觉(Vibration API)。
- 实用性——只在确有必要时添加反馈。将触觉/声音留给有意义的时刻(成功、错误、提交、吸附)。过多反馈会让用户习惯性地忽略所有反馈。
14. 减少动态效果与无障碍
减少动态效果不代表没有反馈——而是改用更柔和、不会引发前庭不适的等效反馈。响应以下三种独立信号,并将其融入组件:
prefers-reduced-motion: reduce——用短暂的透明度交叉淡入淡出或静态过渡替代滑动/弹簧/视差效果。去掉弹性/过冲效果。保留有助于理解的透明度/颜色变化。prefers-reduced-transparency: reduce——让半透明表面更磨砂/更实心:提高背景不透明度,减弱模糊。prefers-contrast: more——使用近乎实心的背景,并添加清晰、对比鲜明的边框。
此外:避免全视口移动背景、缓慢循环振荡(接近 0.2 Hz/每 5s 一个周期)以及亮度骤变(让深色↔浅色主题切换更平缓)。大型移动物体在移动过程中应设为半透明;大型表面大幅移动时先淡出,稳定后再淡入。
@media (prefers-reduced-motion: reduce) {
.sheet { transition: opacity 200ms ease; transform: none !important; }
}
@media (prefers-reduced-transparency: reduce) {
.toolbar { background: white; backdrop-filter: none; }
}
15. 字体排印 — 光学尺寸、字距、行距
Apple 会根据字号改变字体形态;Web 设计也应遵循同样的原则。(摘自 The Details of UI Typography,WWDC 2020。)
- 字距(letter-spacing)应随字号调整,不能所有字号都使用同一个值。 大号展示文本需要负字距(字号增大时,字母看起来会分得太开);小号文本则需要略微增加字距,以提高可读性。固定的
letter-spacing总会有不合适的地方。收紧标题字距,正文保持在接近0。 - 行距(line-height)应随字号增大而反向调整。 大号标题的行距要紧,正文则要宽松些。对于上下伸部较高的文字,应增大行距;对于信息密集的 UI,则应收紧行距。
- **将字重、字号和行距作为一个整体来构建层级,**而不是只调整字号。通过字重强调内容——既能增强视觉分量,又不占用更多空间。
- 尊重用户的文本大小设置(动态字体)。让布局随文本缩放——使用
rem/em设置间距,而非固定的 px——这样较大的字体就不会破坏布局。 - **优先使用平台的系统字体,**而非自定义字体;系统字体已内置光学尺寸、字距表和可读性优化。只有确有理由时才覆盖。
:root { font: 100%/1.5 system-ui, sans-serif; } /* body: system font, comfortable leading */
.display {
font-size: clamp(2rem, 5vw, 4rem);
line-height: 1.05; /* tight leading for large text */
letter-spacing: -0.02em; /* negative tracking as it grows */
font-optical-sizing: auto;
}
16. 设计基础——八项原则
上述动效与工艺服务于 Apple 的八项设计原则(Principles of Great Design,WWDC 2026)。请使用以下名称进行思考:
- 目的。 有意图地创作;决定不做什么。每项功能都会占用用户的时间、注意力并消耗信任——只在能带来回报的地方花费这份预算。
- 自主权。 让人们掌控一切:提供选择,不要强迫他们走单一路径。提供容错能力作为保障——误操作应易于撤销;只有面对真正具有破坏性且不可逆的操作时,才显示确认对话框(谨慎使用;过度使用会让人们习惯性地点过确认)。
- 责任。 以用户利益为先。隐私:在恰当时机提出请求,只索取所需信息,并说明清楚。安全:预判误用和伤害——尤其是 AI 场景(考虑过敏情况的食谱应用不得推荐有害食材)。添加预览、确认和免责声明;如果功能的风险超过其价值,就将其删去。
- 熟悉感。 以人们已有的认知为基础。使用既不过于直白、也不过于抽象的隐喻(垃圾桶表示删除),并遵循人们熟悉的使用规律。保持一致:外观相同的事物,行为必须相同,位置也应相同(在 macOS 上,关闭按钮始终位于左上角),这样人们才能预测接下来会发生什么。只有在能证明更好的情况下,才打破熟悉的模式——然后要进行测试,而不是想当然。
- 灵活性。 为不同的情境、设备和各种能力水平进行设计。适应平台(iPhone 适合快速触控;桌面端适合深度工作流和精确的指针控制),也适应具体情境。进行包容性设计(兼顾年龄、语言、专业水平和无障碍需求)。如果一种布局无法适合所有人,就让人们能够个性化设置——重新排列控件,隐藏不用的内容。
- 简洁——而非极简。 去除不必要的内容,让核心目的更加突出;把所有内容都塞在一个地方看似极简,却并不简洁。表达精炼(使用通俗语言、不用术语、减少步骤),表达清晰(利用层级——顺序、间距、对比度——让最重要的内容最显眼)。每个元素都必须有存在的理由;有时增加上下文反而能让设计更简洁(例如显示剩余时间的视频拖动条)。先展示常见操作路径,将高级选项放在下一层。
- 工艺。 对细节一丝不苟,才能建立信任。精美的排版、适应浅色/深色模式的颜色、清晰的图标,以及能提供即时、自然反馈的响应式动画。没有任何东西是随意的——每个间距、时间和对齐数值都是经过深思熟虑、能够解释的选择。滚动卡顿、图标未对齐,以及旋转设备时崩坏的布局,都会显得漫不经心。工艺需要不断迭代并经得起时间考验——随着功能和硬件变化,持续改进设计。
- 愉悦感。 这是做好其他七项原则后的结果,而不是额外撒上的亮片。确定希望人们感受到什么(平静、自信、兴奋),并在每项决策中强化这种感受。
以下实践规则有助于落实这些原则:
- 反馈分为四类: 状态、完成、警告、错误。确认有意义的操作,展示持续进行的状态,在问题发生前发出警告,并在输入时即时验证(而不是等到提交时才验证)。
- 路径指引。 每个屏幕都应回答:我在哪里?我能去哪里?那里有什么?我该如何离开?绝不要困住用户。
- 分组与映射。 距离近意味着有关联;将控件放在其影响对象附近,并按照控件改变的内容来安排控件。如果需要用标签解释控件的作用,说明映射关系还不够清晰。
- 具体明确的标签胜过稳妥但泛泛的标签。 根据导航项的内容为其命名(“进度”“资料库”),不要使用含糊的统称(“主页”)。具体明确才能让人们有所预期。
17. 流程
- 通过交互原型进行探索——交互式演示胜过“百万张静态设计图”。 通过构建并试用来探索界面;可运行的原型也能设定明确的标准,避免最终实现平平无奇。
- 将交互与视觉设计结合起来。 “你不应该能分辨两者的界限。” 动效不是像素完成后再添加的一层。
- 在真实情境中与真实用户测试,并以全新的视角审视动效——用慢动作或逐帧播放,捕捉全速播放时看不见的细节。
快速参考
| 需求 | 技术 | 具体值 |
|---|---|---|
| 默认 UI 弹簧 | 临界阻尼,无过冲 | damping 1.0,response 0.3–0.4 |
| 动量/轻扫弹簧 | 欠阻尼,轻微回弹 | damping ~0.8,response 0.3–0.4 |
| 手势 → 弹簧速度 | 传递释放速度 | 若已归一化,则使用 gestureVelocity / (target − current) |
| 轻扫落点 | 投射动量 | current + (v/1000)·d/(1−d),d ≈ 0.998 |
| 平滑中断 | 从呈现值(实时值)开始 | 读取屏幕上的变换值 |
| 避免反向时出现“砖墙” | 重新设定目标时保留速度 | 使用可混合速度的弹簧 |
| 可逆转场 | 镜像缓动曲线 | 反向 cubic-bézier |
| 决定反向还是继续 | 使用速度的方向,而非位置 | 在释放时判断 |
| 1:1 拖动 | Pointer Events + 捕获 | 保留抓取偏移量 |
| 反馈 | 按下指针时持续提供 | 绝不能只在结束时提供 |
| 边界 | 使用橡皮筋效果,不要硬性停止 | 逐步增加阻力 |
| 半透明界面元素 | backdrop-filter 图层 | 内容在其下方滚动 |
| 字距 | 根据字号设置,绝不固定 | 收紧大号文本(-0.02em),正文接近 0 |
| 减少动态效果 | 淡入淡出,不要滑动/弹簧 | @media (prefers-reduced-motion) |