Apple Design
Comment Apple construit des interfaces qui cessent de ressembler à un ordinateur et commencent à ressentir comme une extension de vous-même. Ces connaissances proviennent des présentations de design d'Apple à la WWDC — principalement Designing Fluid Interfaces (WWDC 2018) — distillées et traduites pour la plateforme web (CSS, Pointer Events, requestAnimationFrame, bibliothèques de ressorts comme Motion/Framer Motion).
Le fil conducteur : une interface semble vivante lorsque le mouvement part de la valeur actuelle à l'écran, hérite de la vélocité de l'utilisateur, projette l'élan vers l'avant et peut être saisie et inversée à tout instant. Les ressorts sont l'outil qui rend tout cela naturel, car ils sont intrinsèquement interruptibles et conscients de la vélocité.
L'idée centrale
« Lorsque nous alignons l'interface sur notre façon de penser et de bouger, quelque chose de magique se produit — elle cesse de ressembler à un ordinateur et commence à ressentir comme une extension transparente de nous-mêmes. »
Une interface est fluide lorsqu'elle se comporte comme le monde physique : les choses répondent instantanément, bougent en continu, portent de l'élan, résistent aux limites et peuvent être redirigées en plein mouvement. Tout ce qui suit est un moyen de s'en rapprocher.
Apple conçoit le design comme servant quatre besoins humains : la sécurité/la prévisibilité, la compréhension, l'accomplissement et la joie. Chaque règle ici sert l'un d'eux.
1. Réponse — éliminer la latence
Dès que le décalage apparaît, la sensation de directeté « tombe d'une falaise ». La réponse est le fondement sur lequel tout le reste est construit.
- Répondez au pointeur-enfoncé, pas au relâchement. Mettez en surbrillance un bouton dès qu'il est pressé. Attendre le
click/le relâchement du toucher pour montrer un feedback semble mort. - Soyez vigilant face à chaque latence. Auditez les « debounces », les temporisateurs artificiels, les délais de transition et le délai de ~300 ms du tap. Tout ce qui se trouve sur le chemin d'entrée et qui n'est pas essentiel est une régression.
- Le feedback doit être continu pendant l'interaction, pas seulement à la fin. Pour un glissement, un curseur ou un tiroir, mettez à jour l'UI 1:1 avec le pointeur tout au long du parcours — n'animez jamais uniquement lorsque le geste est terminé.
/* Le feedback vit sur la pression, et il est instantané */
.button:active {
transform: scale(0.97);
transition: transform 100ms ease-out;
}
2. Manipulation directe — suivi 1:1
« Le toucher et le contenu doivent bouger ensemble. »
Lorsque l'utilisateur fait glisser quelque chose, cela doit rester collé au doigt — et respecter le décalage par rapport à l'endroit où il l'a saisi. Accrocher à l'élément au centre lors de la saisie brise l'illusion immédiatement.
- Utilisez Pointer Events avec
setPointerCapturepour que le suivi continue même lorsque le pointeur quitte les limites de l'élément. - Suivez un court historique de vélocité/position (les derniers événements
pointermove), pas seulement le point actuel — vous aurez besoin de la vélocité lors du relâchement.
el.addEventListener('pointerdown', (e) => {
el.setPointerCapture(e.pointerId);
const grabOffset = e.clientY - el.getBoundingClientRect().top; // respecter l'endroit où ils ont saisi
// ...suivre l'historique de position + timestamp pour la vélocité
});
3. Interruptibilité — le principe le plus important
« La pensée et le geste se produisent en parallèle. »
Chaque animation doit être interruptible et redirigeable à tout moment. Un utilisateur doit pouvoir saisir un élément en mouvement en plein vol et l'inverser sans attendre la fin de l'animation. Une fenêtre modale en cours de fermeture que l'utilisateur saisit à nouveau doit suivre le doigt — pas d'abord se fermer complètement, puis se rouvrir.
- Ne verrouillez jamais l'entrée pendant une transition.
- Animez toujours à partir de la valeur de présentation (actuelle), jamais de la valeur cible. Lors d'une interruption, lisez la transformation en direct de l'élément et démarrez la nouvelle animation à partir de là. Partir de la valeur logique/cible provoque un saut visible.
- Évitez les transitions CSS et les
@keyframespour tout ce qui est piloté par le geste — ils ne peuvent pas être saisis facilement et inversés en plein vol. Les ressorts animent à partir de la valeur actuelle par défaut, ce qui est exactement ce dont l'interruption a besoin. - Lorsqu'un geste s'inverse, mélangez la vélocité — ne la coupez pas brusquement. Remplacer une animation par une autre lors d'une inversion crée une discontinuité de vélocité, un « mur de briques ». Les bibliothèques de ressorts qui transportent la vélocité lors d'un ré-ciblage l'évitent. (C'est ce que font les animations additives d'iOS nativement ; sur le web, choisissez une bibliothèque de ressorts qui se ré-cible à partir de la vélocité actuelle.)
- Décomposez le mouvement 2D en ressorts X et Y indépendants. Un seul ressort sur une distance 2D se désynchronise lorsque X et Y ont des vélocités différentes.
4. Comportement plutôt qu'animation — utilisez des ressorts
« Pensez à l'animation comme à une conversation entre vous et l'objet, pas quelque chose de prescrit par l'interface. »
Une animation pré-écrite à durée fixe ne peut pas répondre à une nouvelle entrée. Un ressort le peut — une nouvelle entrée change simplement la cible, et le mouvement reste continu. Utilisez des ressorts pour tout ce qu'un utilisateur peut toucher.
Apple a délibérément remplacé le triplet physique (masse/raideur/amortissement) par deux paramètres adaptés aux concepteurs. Pensez en ces termes :
- Rapport d'amortissement — contrôle le dépassement.
1.0= amortissement critique, pas de rebond, stabilisation lisse.< 1.0= dépasse et oscille. Plus bas = plus rebondissant. - Réponse — la rapidité avec laquelle la valeur atteint la cible, en secondes. Plus bas = plus vif. Ce n'est pas la « durée » — un ressort n'a pas de durée fixe ; son temps de stabilisation émerge des paramètres.
Valeurs par défaut :
- Commencez la plupart des IU à amortissement
1.0(amortissement critique) — élégant et non distrayant. - Ajoutez du rebond (amortissement ~
0.8) uniquement lorsque le geste lui-même portait de l'élan (un flick, un lancer, un relâchement de glissement). Un dépassement sur un menu qui vient de s'ouvrir semble faux ; un dépassement sur une carte que vous avez lancée semble juste.
Valeurs concrètes fournies par Apple :
| Interaction | Amortissement | Réponse |
|---|---|---|
| Déplacer / repositionner (ex. PiP) | 1.0 | 0.4 |
| Rotation | 0.8 | 0.4 |
| Tiroir / feuille | 0.8 | 0.3 |
Cartographie web (Motion / Framer Motion) : l'API ressort bounce + duration correspond étroitement à l'amortissement et la réponse d'Apple. Un style de base sûr est d'utiliser des ressorts damping: 1.0 partout par défaut ; réservez le rebond pour les interactions physiques pilotées par l'élan.
import { animate } from 'motion';
// Amortissement critique par défaut (pas de dépassement)
animate(el, { y: 0 }, { type: 'spring', bounce: 0, duration: 0.4 });
// Interaction avec élan — un peu de rebond, uniquement parce qu'un flick l'a précédé
animate(el, { y: target }, { type: 'spring', bounce: 0.2, duration: 0.4 });
5. Transfert de vélocité — la jointure entre le glissement et l'animation
Lorsqu'un geste se termine, l'animation doit continuer à la vélocité exacte du doigt, afin qu'il n'y ait pas de jointure visible entre le glissement et l'animation. C'est le détail qui sépare le plus le « fluide » du « acceptable ».
Passez la vélocité de relâchement du pointeur comme vélocité initiale du ressort. Certaines API de ressort veulent une vélocité relative — normalisez-la par la distance restante jusqu'à la cible :
relativeVelocity = gestureVelocity / (targetValue − currentValue)
Exemple : élément à y=50, cible y=150 (100 px à parcourir), doigt se déplaçant à 50 px/s → vélocité initiale du ressort = 50 / 100 = 0.5. Framer Motion / Motion acceptent directement la vélocité absolue en px/s (option velocity), vous lui passez donc généralement la valeur brute.
6. Projection d'élan — animer vers où le geste va
« Prenez une petite entrée et faites-en une grande sortie. »
Ne vous accrochez pas à la limite la plus proche à partir du point de relâchement. Utilisez la vélocité pour projeter la position de repos — exactement comme la décélération de défilement — puis accrochez-vous à la cible la plus proche de ce point projeté. C'est ce qui fait qu'un flick semble lancer l'élément.
La fonction de projection exacte d'Apple (d'après le code d'exemple de Designing Fluid Interfaces) :
// decelerationRate ≈ 0.998 pour une sensation de défilement normale ; 0.99 pour plus vif
function project(initialVelocity /* px/s */, decelerationRate = 0.998) {
return (initialVelocity / 1000) * decelerationRate / (1 - decelerationRate);
}
const projectedEndpoint = currentPosition + project(releaseVelocity);
const target = nearestSnapPoint(projectedEndpoint); // choisir la cible à partir de la projection
animateSpringTo(target, { velocity: releaseVelocity }); // puis transférer la vélocité (§5)
Remarque : le v²/(2·decel) du manuel de physique n'est pas ce qu'Apple utilise — utilisez la forme de décroissance exponentielle ci-dessus. C'est le comportement standard dans les bons feuillets inférieurs et carrousels (Vaul, Embla).
7. Cohérence spatiale — chemins symétriques, origines ancrées
« Si quelque chose disparaît d'une certaine manière, nous nous attendons à ce qu'il émerge de là d'où il vient. »
- Entrez et sortez par le même chemin. Un panneau qui glisse depuis la droite doit se fermer vers la droite. Entrer par la droite / sortir par le bas semble déconnecté et déroutant.
- Ancrez les interactions à leur source. Un menu, un popover ou une feuille doit provenir de l'élément qui l'a déclenché — définissez
transform-originsur le déclencheur, afin que la relation spatiale entre le bouton et le contenu soit évidente. (C'est le même point de conscience de l'origine que les popovers qui se déploient à partir de leur déclencheur, pas de leur centre.) - Reflétez l'assouplissement sur les transitions réversibles afin que le chemin de sortie corresponde au chemin de retour (utilisez des points de contrôle cubiques de Bézier inverses pour les deux directions).
8. Indice dans la direction du geste
Les humains prédisent un état final à partir d'une trajectoire. Le mouvement intermédiaire doit indiquer où les choses vont — les modules du Centre de contrôle « grandissent vers le haut et vers l'extérieur en direction de votre doigt. » Faites en sorte que les images intermédiaires pointent vers le résultat, pas seulement interpoler aveuglément vers celui-ci.
9. Effet « rubber-banding » — limites douces
À une limite, résistez progressivement plutôt que de vous arrêter brusquement. Un arrêt brusque est perçu comme « gelé » ; une résistance continue est perçue comme « réactif, mais il n'y a rien de plus ici. » Appliquez un amortissement qui augmente plus l'utilisateur dépasse la limite.
// Plus on dépasse la limite, moins l'élément suit — les vraies choses ralentissent avant de s'arrêter
function rubberband(overshoot, dimension, constant = 0.55) {
return (overshoot * dimension * constant) / (dimension + constant * Math.abs(overshoot));
}
10. Détails de conception des gestes (la liste de contrôle « sensation »)
- Tap : mettez en surbrillance au toucher-enfoncé (instantané), validez au toucher-relâché. Ajoutez environ 10 px d'hystérésis/de marge de hit autour de la cible, et permettez l'annulation en glissant ailleurs et en revenant.
- Glissement/balayage : exigez un petit seuil de mouvement (hystérésis, ~10 px) avant de vous engager dans une direction, puis suivez 1:1.
- Détectez tous les gestes plausibles en parallèle dès le premier mouvement, puis annulez en toute confiance les perdants une fois l'intention claire. Évitez les reconnaisseurs qui ne rapportent qu'un état final (événements de type
swipeleft) — ils jettent le suivi continu dont vous avez besoin pour le feedback. - Minimisez les délais de désambiguïsation. La détection de double-tap retarde inévitablement les simples tap ; ne payez ce coût que là où le double-tap existe vraiment.
11. Fluidité au niveau de la frame
La fluidité concerne ce qui se trouve dans les images, pas seulement le taux d'images par seconde.
- Maintenez le changement de position par frame en dessous du seuil de perception pour éviter le scintillement.
- Pour les mouvements très rapides, un flou de mouvement / étirement subtil encode la vitesse et est mieux perçu qu'une traînée nette et dure.
requestAnimationFrameest l'horloge synchronisée à l'affichage du web (Apple utiliseCADisplayLink). Animez uniquement les propriétés compatibles avec le compositeur —transformetopacity— et faites un indice avecwill-changelà où le mouvement est imminent.
12. Matériaux et profondeur — la translucidité transmet la hiérarchie
Apple utilise des matériaux translucides comme une couche fonctionnelle flottante qui apporte de la structure sans voler l'attention. Sur le web, approchez-vous avec backdrop-filter.
- Construisez les barres de navigation/barres d'outils/feuilles comme des couches translucides (
backdrop-filter: blur()+ un fond semi-transparent) avec le contenu défilant en dessous — pas des barres opaques qui consomment une bande fixe. - Le poids du matériau encode la hiérarchie : les matériaux plus foncés/plus lourds séparent les régions structurelles (barres latérales) ; les matériaux plus clairs attirent l'attention sur les éléments interactifs (boutons). N'empilez jamais une surface translucide claire sur une autre — la lisibilité s'effondre.
- Les surfaces plus grandes doivent être perçues comme plus épaisses : un flou plus fort + une ombre plus profonde que les petits éléments. Considérez une ombre contextuelle — plus lourde sur un contenu chargé/texté pour la séparation, plus légère sur des fonds unis.
- Assombrissez pour concentrer, séparez pour maintenir le flux. Une tâche modale associe la surface à un écran d'assombrissement et repousse le fond vers l'arrière/le bas. Un panneau parallèle non bloquant utilise la translucidité et le décalage sans écran d'assombrissement afin que le flux ne soit pas interrompu. Pour les feuilles empilées, assombrissez et repoussez progressivement chaque couche parente.
- La vibrancy maintient la lisibilité du texte sur des fonds changeants. Sur les surfaces floutées/translucides, n'utilisez pas de texte gris uni — utilisez un contraste plus élevé, un poids légèrement plus lourd et une légère augmentation de l'espacement des lettres. Mettez la couleur sur une couche solide, pas sur le premier plan translucide.
- Effets de bord de défilement, pas de séparateurs durs. Au lieu d'une bordure de 1 px sous un en-tête collant, dégradez un petit masque de flou/dégradé là où le contenu rencontre le chrome flottant — uniquement là où l'IU flottante chevauche réellement le contenu.
- Matérialisez, ne vous contentez pas d'atténuer. Pour les surfaces en verre/flou, animez le rayon de flou et l'échelle ensemble à l'entrée/à la sortie, afin que la surface soit perçue comme un matériau réel qui arrive plutôt qu'un fondu d'opacité uni.
.toolbar {
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(20px) saturate(180%);
border-top: 1px solid rgba(255, 255, 255, 0.4); /* bord supérieur lumineux = lumière attrapant le matériau */
}
13. Feedback multimodal — mouvement + son + haptique
Trois règles pour combiner les sens (d'après Designing Audio-Haptic Experiences) :
- Causalité — il doit être évident ce qui a causé le feedback. Déclenchez-le sur l'événement causal réel (le basculement du commutateur, l'élément qui s'encastre), et faites correspondre son caractère à la physicalité de l'action.
- Harmonie — le visuel, le son et l'haptique doivent se déclencher sur la même image. La latence entre eux détruit l'illusion. Ne laissez pas une transition CSS prendre du retard sur l'audio/l'haptique (API Vibration).
- Utilité — ajoutez du feedback uniquement là où il mérite sa place. Réservez l'haptique/le son pour les moments significatifs (succès, erreur, validation, enclenchement). Le trop-plein de feedback apprend aux utilisateurs à tout ignorer.
14. Mouvement réduit et accessibilité
Mouvement réduit ne signifie pas absence de feedback