mirror of
https://github.com/emilkowalski/skills.git
synced 2026-07-24 12:44:08 +08:00
apple skill
This commit is contained in:
@@ -35,4 +35,5 @@ This is your shortcut to great interfaces. A shortcut to stand out in a sea of s
|
||||
|
||||
- **[emil-design-eng](./skills/emil-design-eng/SKILL.md)** — The main skill that consinsts of mostly animation, but also some design advice.
|
||||
- **[review-animations](./skills/review-animations/SKILL.md)** — Review your animations in a strict way, based on my rules.
|
||||
- **[apple-design](./skills/apple-design/SKILL.md)** — Apple's principles for fluid, physical interfaces, distilled from their WWDC design talks and translated for the web.
|
||||
- More coming very soon.
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
---
|
||||
name: apple-design
|
||||
description: Apple's principles for fluid, physical interfaces — distilled from WWDC talks (Designing Fluid Interfaces, Essential Design Principles, The Qualities of Great Design, Advanced Animations with UIKit, and Apple's motion/materials guidance) and translated for building web UIs. Covers direct manipulation, interruptible spring motion, velocity handoff, momentum projection, spatial consistency, materials/depth, multimodal feedback, and reduced-motion.
|
||||
---
|
||||
|
||||
# Apple Design
|
||||
|
||||
How Apple builds interfaces that stop feeling like a computer and start feeling like an extension of you. This knowledge comes from Apple's WWDC design talks — chiefly *Designing Fluid Interfaces* (WWDC 2018) — distilled and translated into the web platform (CSS, Pointer Events, `requestAnimationFrame`, spring libraries like Motion/Framer Motion).
|
||||
|
||||
The through-line: **an interface feels alive when motion starts from the current on-screen value, inherits the user's velocity, projects momentum forward, and can be grabbed and reversed at any instant.** Springs are the tool that makes all of this natural, because they are inherently interruptible and velocity-aware.
|
||||
|
||||
## The Core Idea
|
||||
|
||||
> "When we align the interface to the way we think and move, something magical happens — it stops feeling like a computer and starts feeling like a seamless extension of us."
|
||||
|
||||
An interface is fluid when it behaves like the physical world: things respond instantly, move continuously, carry momentum, resist at boundaries, and can be redirected mid-motion. Everything below is a way to get closer to that.
|
||||
|
||||
Apple frames design as serving four human needs: **safety/predictability, understanding, achievement, and joy.** Every rule here serves one of them. The only thing that matters is how well the interface satisfies the emotional and practical needs of the person using it.
|
||||
|
||||
## 1. Response — kill latency
|
||||
|
||||
The moment lag appears, the feeling of directness "falls off a cliff." Response is the foundation everything else is built on.
|
||||
|
||||
- **Respond on pointer-down, not on release.** Highlight a button the instant it's pressed. Waiting for `click`/touch-up to show feedback feels dead.
|
||||
- **Be vigilant about every latency.** Audit debounces, artificial timers, transition waits, and the ~300ms tap delay. Anything on the input path that isn't essential is a regression.
|
||||
- **Feedback must be continuous *during* the interaction, not just at the end.** For a drag, slider, or drawer, update the UI 1:1 with the pointer the whole way through — never animate only when the gesture completes.
|
||||
|
||||
```css
|
||||
/* Feedback lives on the press, and it's instant */
|
||||
.button:active {
|
||||
transform: scale(0.97);
|
||||
transition: transform 100ms ease-out;
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Direct manipulation — 1:1 tracking
|
||||
|
||||
> "Touch and content should move together."
|
||||
|
||||
When the user drags something, it must stay glued to the finger — and respect the offset from *where they grabbed it*. Snapping to the element's center on grab breaks the illusion immediately.
|
||||
|
||||
- Use Pointer Events with `setPointerCapture` so tracking continues even when the pointer leaves the element's bounds.
|
||||
- Track a short **velocity/position history** (last few `pointermove` events), not just the current point — you'll need velocity at release.
|
||||
|
||||
```js
|
||||
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. Interruptibility — the single most important principle
|
||||
|
||||
> "The thought and the gesture happen in parallel."
|
||||
|
||||
Every animation must be interruptible and redirectable at any moment. A user must be able to grab a moving element mid-flight and reverse it without waiting for the animation to finish. A closing modal the user grabs again should follow the finger — not finish closing first, then reopen.
|
||||
|
||||
- **Never lock out input during a transition.**
|
||||
- **Always animate from the *presentation* (current) value, never the target value.** On interrupt, read the element's live on-screen transform and start the new animation from there. Starting from the logical/target value causes a visible jump.
|
||||
- **Avoid CSS transitions and `@keyframes` for anything gesture-driven** — they can't be smoothly grabbed and reversed mid-flight. Springs animate from the current value by default, which is exactly what interruption needs.
|
||||
- **When a gesture reverses, blend velocity — don't hard-cut it.** Replacing one animation with another at a reversal creates a velocity discontinuity, a "brick wall." Spring libraries that carry velocity through a re-target avoid it. (This is what iOS's *additive animations* do natively; on the web, choose a spring library that re-targets from the current velocity.)
|
||||
- **Decompose 2D motion into independent X and Y springs.** A single spring on a 2D distance desyncs when X and Y have different velocities.
|
||||
|
||||
## 4. Behavior over animation — use springs
|
||||
|
||||
> "Think of animation as a conversation between you and the object, not something prescribed by the interface."
|
||||
|
||||
A pre-scripted, fixed-duration animation can't respond to new input. A spring can — new input just changes the target, and the motion stays continuous. Reach for springs for anything a user can touch.
|
||||
|
||||
Apple deliberately replaced the physics triplet (mass/stiffness/damping) with two designer-friendly parameters. Think in these:
|
||||
|
||||
- **Damping ratio** — controls overshoot. `1.0` = critically damped, no bounce, smooth settle. `< 1.0` = overshoots and oscillates. Lower = bouncier.
|
||||
- **Response** — how quickly the value reaches the target, in seconds. Lower = snappier. **This is not "duration"** — a spring has no fixed duration; its settle time emerges from the parameters.
|
||||
|
||||
**Defaults:**
|
||||
- Start most UI at **damping `1.0`** (critically damped) — graceful and non-distracting.
|
||||
- Add bounce (**damping ~`0.8`**) **only when the gesture itself carried momentum** (a flick, a throw, a drag release). Overshoot on a menu that just faded in feels wrong; overshoot on a card you flicked feels right.
|
||||
|
||||
**Concrete values Apple ships:**
|
||||
|
||||
| Interaction | Damping | Response |
|
||||
| --- | --- | --- |
|
||||
| Move / reposition (e.g. PiP) | `1.0` | `0.4` |
|
||||
| Rotation | `0.8` | `0.4` |
|
||||
| Drawer / sheet | `0.8` | `0.3` |
|
||||
|
||||
**Web mapping (Motion / Framer Motion):** the `bounce` + `duration` spring API maps closely to Apple's damping + response. A safe house style is `damping: 1.0` springs everywhere by default; reserve bounce for momentum-driven, physical interactions.
|
||||
|
||||
```js
|
||||
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. Velocity handoff — the seam between drag and animation
|
||||
|
||||
When a gesture ends, the animation must **continue at the finger's exact velocity**, so there's no visible seam between dragging and animating. This is the detail that most separates "fluid" from "fine."
|
||||
|
||||
Pass the pointer's release velocity as the spring's initial velocity. Some spring APIs want **relative** velocity — normalize it by the remaining distance to the target:
|
||||
|
||||
```
|
||||
relativeVelocity = gestureVelocity / (targetValue − currentValue)
|
||||
```
|
||||
|
||||
Example: element at `y=50`, target `y=150` (100px to go), finger moving 50px/s → initial spring velocity = `50 / 100 = 0.5`. Framer Motion / Motion take absolute px/s velocity directly (`velocity` option), so you usually hand it the raw value.
|
||||
|
||||
## 6. Momentum projection — animate to where the gesture is *going*
|
||||
|
||||
> "Take a small input and make a big output."
|
||||
|
||||
Don't snap to the nearest boundary from the *release point*. Use velocity to **project the resting position** — exactly like scroll deceleration — then snap to the target nearest that projected point. This is what makes a flick feel like it throws the element.
|
||||
|
||||
Apple's exact projection function (from the *Designing Fluid Interfaces* sample code):
|
||||
|
||||
```js
|
||||
// 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)
|
||||
```
|
||||
|
||||
Note: the physics-textbook `v²/(2·decel)` is *not* what Apple ships — use the exponential-decay form above. This is the standard behavior in good bottom-sheets and carousels (Vaul, Embla).
|
||||
|
||||
## 7. Spatial consistency — symmetric paths, anchored origins
|
||||
|
||||
> "If something disappears one way, we expect it to emerge from where it came."
|
||||
|
||||
- **Enter and exit along the same path.** A panel that slides in from the right must dismiss to the right. In-from-right / out-the-bottom feels disconnected and confusing.
|
||||
- **Anchor interactions to their source.** A menu, popover, or sheet should originate from the element that triggered it — set `transform-origin` to the trigger, so the spatial relationship between button and content is obvious. (This is the same origin-awareness point as popovers scaling from their trigger, not their center.)
|
||||
- **Mirror the easing on reversible transitions** so the outbound path matches the return path (use inverse cubic-bézier control points for the two directions).
|
||||
|
||||
## 8. Hint in the direction of the gesture
|
||||
|
||||
Humans predict a final state from a trajectory. Intermediate motion should telegraph where things are going — Control Center modules "grow up and out toward your finger." Make the in-between frames point at the outcome, not just interpolate blindly to it.
|
||||
|
||||
## 9. Rubber-banding — soft boundaries
|
||||
|
||||
At an edge, resist progressively instead of stopping hard. A hard stop reads as "frozen"; continuous resistance reads as "responsive, but there's nothing more here." Apply damping that increases the further past the boundary the user drags.
|
||||
|
||||
```js
|
||||
// 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. Gesture design details (the "feel" checklist)
|
||||
|
||||
- **Tap:** highlight on touch-*down* (instant), commit on touch-*up*. Add ~10px of hysteresis/hit padding around the target, and allow cancel-by-dragging-away and back.
|
||||
- **Drag/swipe:** require a small movement threshold (hysteresis, ~10px) before committing to a direction, then track 1:1.
|
||||
- **Detect all plausible gestures in parallel from the first move**, then confidently cancel the losers once intent is clear. Avoid recognizers that only report a *final* state (`swipeleft`-type events) — they throw away the continuous tracking you need for feedback.
|
||||
- **Minimize disambiguation delays.** Double-tap detection unavoidably delays single taps; only pay that cost where double-tap truly exists.
|
||||
|
||||
## 11. Frame-level smoothness
|
||||
|
||||
Smoothness is about *what's in the frames*, not just the frame rate.
|
||||
|
||||
- Keep the per-frame positional change below the perception threshold to avoid strobing.
|
||||
- For very fast motion, a subtle **motion blur / stretch** encodes speed and reads better than a hard sharp streak.
|
||||
- `requestAnimationFrame` is the web's display-synced clock (Apple uses `CADisplayLink`). Animate only compositor-friendly properties — `transform` and `opacity` — and hint with `will-change` where motion is imminent.
|
||||
|
||||
## 12. Materials & depth — translucency conveys hierarchy
|
||||
|
||||
Apple uses translucent materials as a floating functional layer that brings structure without stealing focus. On the web, approximate with `backdrop-filter`.
|
||||
|
||||
- **Build nav/toolbars/sheets as translucent layers** (`backdrop-filter: blur()` + a semi-transparent background) with content scrolling underneath — not opaque bars that consume a fixed strip.
|
||||
- **Material weight encodes hierarchy:** darker/heavier materials separate structural regions (sidebars); lighter materials draw attention to interactive elements (buttons). **Never stack a light translucent surface on another** — legibility collapses.
|
||||
- **Bigger surfaces should read as thicker:** stronger blur + a deeper shadow than small chips. Consider context-aware shadow — heavier over busy/text content for separation, lighter over plain backgrounds.
|
||||
- **Dim to focus, separate to keep flow.** A modal task pairs the surface with a dimming scrim and pushes the background back/down. A parallel, non-blocking panel uses translucency and offset *without* a scrim so the flow isn't broken. For stacked sheets, progressively dim and push back each parent layer.
|
||||
- **Vibrancy keeps text legible over changing backgrounds.** Over blurred/translucent surfaces, don't use flat gray text — use higher-contrast, slightly heavier weight, and a small letter-spacing bump. Put color on a solid layer, not the translucent foreground.
|
||||
- **Scroll edge effects, not hard dividers.** Instead of a 1px border under a sticky header, fade a small blur/gradient mask where content meets floating chrome — only where floating UI actually overlaps content.
|
||||
- **Materialize, don't just fade.** For glass/blur surfaces, animate blur radius and scale together on enter/exit, so the surface reads as a real material arriving rather than a plain opacity fade.
|
||||
|
||||
```css
|
||||
.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. Multimodal feedback — motion + sound + haptics
|
||||
|
||||
Three rules for combining senses (from *Designing Audio-Haptic Experiences*):
|
||||
|
||||
1. **Causality** — it must be obvious what caused the feedback. Trigger it on the actual causal event (the toggle flipping, the item snapping home), and match its character to the action's physicality.
|
||||
2. **Harmony** — the visual, the sound, and the haptic must fire on the **same frame**. Latency between them destroys the illusion. Don't let a CSS transition lag the audio/haptic (Vibration API).
|
||||
3. **Utility** — add feedback only where it earns its place. Reserve haptics/sound for meaningful moments (success, error, commit, snap). Over-feedback trains users to ignore all of it.
|
||||
|
||||
## 14. Reduced motion & accessibility
|
||||
|
||||
Reduced motion doesn't mean *no* feedback — it means a gentler, non-vestibular equivalent. Respond to three independent signals and bake them into your components:
|
||||
|
||||
- **`prefers-reduced-motion: reduce`** — replace slides/springs/parallax with short opacity **cross-fades or static transitions**. Drop elastic/overshoot. Keep opacity/color changes that aid comprehension.
|
||||
- **`prefers-reduced-transparency: reduce`** — make translucent surfaces frostier/solid: raise background opacity, drop the blur.
|
||||
- **`prefers-contrast: more`** — near-solid backgrounds with a defined, contrasting border.
|
||||
|
||||
Also: avoid full-viewport moving backgrounds, slow looping oscillations (near 0.2 Hz / one cycle per 5s), and abrupt brightness jumps (ease dark↔light theme changes). Make large moving objects semi-transparent while they travel, and fade big surfaces out during a large reposition and back in once settled.
|
||||
|
||||
```css
|
||||
@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. Design foundations (from Apple's philosophy talks)
|
||||
|
||||
The motion above sits on top of durable design principles:
|
||||
|
||||
- **Match the user's mental model.** Intuitive means "matches what I already expect." Only break a familiar pattern if you can prove it's objectively better — then test it, don't assume.
|
||||
- **Implement metaphors fully.** A metaphor immerses only if you honor its physics. Extend it for real utility or delight; never bend it for novelty.
|
||||
- **Direct, specific labels beat safe generic ones.** Name nav items for their actual contents ("Progress", "Library"), not vague umbrellas ("Home"). Specificity creates predictability.
|
||||
- **Progressive disclosure / 80-20.** Show the common path first; put advanced options one level deeper. Reduce to one primary action per context.
|
||||
- **Grouping & mapping.** Proximity implies relationship; place a control near what it affects; arrange controls to mirror what they change. If you need a label to explain a control, the mapping is weak.
|
||||
- **Four kinds of feedback:** status, completion, warning, error. Confirm meaningful actions, expose ongoing status, warn before problems, validate inline (not on submit).
|
||||
- **Wayfinding.** Every screen should answer: Where am I? Where can I go? What's there? How do I get out? Never trap the user.
|
||||
- **Forgiveness.** Prefer undo over confirmation dialogs; make destructive actions reversible; design so the wrong move is hard to make and easy to recover from.
|
||||
- **Consistency**, internal and external. Reuse the same term/icon/interaction for the same meaning; adopt platform conventions unless you have a strong reason not to.
|
||||
- **Restraint.** Quality "doesn't try to do more than it needs to." Nothing is random — every spacing, timing, and alignment value is a deliberate choice you can defend. Make committed, specific choices instead of averaging toward blandness; offer customization at the edges, not by diluting the core.
|
||||
|
||||
## 16. Process
|
||||
|
||||
- **Prototype interactively — an interactive demo is worth "a million static designs."** You discover the interface by building and playing with it; a working prototype also sets a concrete bar that prevents a mediocre final implementation.
|
||||
- **Design interaction and visuals together.** "You shouldn't be able to tell where one ends and the other begins." Motion is not a layer added after the pixels.
|
||||
- **Test with real people in real context**, and review motion with fresh eyes — play it in slow motion / frame-by-frame to catch what's invisible at full speed.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Need | Technique | Concrete value |
|
||||
| --- | --- | --- |
|
||||
| Default UI spring | Critically damped, no overshoot | `damping 1.0`, `response 0.3–0.4` |
|
||||
| Momentum / flick spring | Under-damped, slight bounce | `damping ~0.8`, `response 0.3–0.4` |
|
||||
| Gesture → spring velocity | Hand off release velocity | `gestureVelocity / (target − current)` if normalized |
|
||||
| Flick landing point | Project momentum | `current + (v/1000)·d/(1−d)`, `d ≈ 0.998` |
|
||||
| Interrupt cleanly | Start from presentation (live) value | read the on-screen transform |
|
||||
| Avoid reversal "brick wall" | Carry velocity through re-target | spring that blends velocity |
|
||||
| Reversible transition | Mirror the easing curve | inverse cubic-bézier |
|
||||
| Decide reverse vs. commit | Use velocity **sign**, not position | at release |
|
||||
| 1:1 drag | Pointer Events + capture | respect the grab offset |
|
||||
| Feedback | On pointer-down, continuous | never only at the end |
|
||||
| Boundary | Rubber-band, don't hard-stop | progressive resistance |
|
||||
| Translucent chrome | `backdrop-filter` layer | content scrolls under |
|
||||
| Reduced motion | Cross-fade, not slide/spring | `@media (prefers-reduced-motion)` |
|
||||
Reference in New Issue
Block a user