关于 apple-design
基于 Apple 流畅界面设计指导的 Web 界面技能,帮助设计师和开发者使用 Web 技术构建即时响应、可中断的手势动画。
主要功能
- 01即时响应用户输入
- 02直接跟踪指针手势
- 03中断并重定向动画
- 04使用弹簧并传递速度
- 05按动量投射到吸附点
- 06在边界实现橡皮筋效果
适用场景
- 01实现可拖动面板和抽屉
- 02制作可中断的手势动画
- 03调整甩动与吸附行为
- 04添加响应式边缘阻力
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ boraoztunc/ skills --skill apple-design安装后在 Claude Code 中输入/apple-design确认已加载。
详细介绍
Apple Design
Apple 如何构建界面,使其不再像一台计算机,而开始像是你自身的延伸。这些知识源自 Apple 的 WWDC 设计演讲——主要是 Designing Fluid Interfaces(WWDC 2018)——并经过提炼,转译到 Web 平台(CSS、Pointer Events、requestAnimationFrame、Motion/Framer Motion 等弹簧库)。
贯穿始终的主线是:当动效从当前屏幕上的值开始、继承用户的速度、向前投射动量,并且能在任意瞬间被抓住并反向时,界面就会显得有生命。 弹簧正是让这一切变得自然的工具,因为它们天生可中断、能感知速度。
核心理念
“当我们让界面与我们的思考和移动方式对齐时,神奇的事情就会发生——它不再像一台计算机,而开始像是我们自身的无缝延伸。”
当一个界面的行为像物理世界时,它就是流畅的:物体即时响应、连续运动、携带动量、在边界处形成阻力,并且能在运动途中被改变方向。下文的一切都是为了更接近这一点。
Apple 将设计框定为服务于四种人类需求:安全感/可预测性、理解、成就感和愉悦感。 这里的每一条规则都服务于其中之一。
1. 响应——消除延迟
一旦出现滞后,直接感就会“断崖式下跌”。响应是其他一切赖以建立的基础。
- 在 pointer-down 时响应,而不是在释放时。 按钮被按下的瞬间就高亮它。等到
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 具有不同速度时,作用于 2D 距离的单一弹簧会失步。
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,则收紧行距。
- 用字重 + 字号 + 行距作为一套来构建层级, 而不只是字号。用字重来强调 — 它能在不占用更多空间的情况下增加存在感。
- 尊重用户的文字大小设置(Dynamic Type)。让布局随文字缩放 — 间距用
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) |