Skills/motion-spring-physics/SKILL.md

5.7 KiB
Raw Permalink Blame History

name description
motion-spring-physics Implement physical, tactile micro-interactions and layout transitions using Spring Physics (Motion / Framer Motion, FLIP technique via layoutId, CSS @starting-style, View Transitions API) with strict compliance with prefers-reduced-motion.

Motion & Spring Physics Architecture

Ce skill régit l'animation interactive haute fidélité. Il remplace les animations rigides basées sur des durées fixes (transition: all 0.3s ease) par des ressorts physiques réels (Spring Physics), conférant aux interfaces un toucher organique, réactif et interruptible caractéristique des applications comme Linear, Raycast ou macOS.


🎯 Déclencheurs d'Activation

Activez ce skill dès que :

  1. Vous animez des éléments interactifs (boutons, modales, tiroirs, onglets, menus accordéons).
  2. Vous concevez des transitions d'état de liste ou de réorganisation (glisser-déposer, suppression, filtrage).
  3. Vous implémentez un indicateur de sélection flottant ou un curseur d'onglets partagé (layoutId).
  4. Vous créez des micro-interactions au survol ou à la pression tactile (whileHover, whileTap).
  5. Vous devez garantir l'accessibilité des animations face au critère WCAG 2.3.3 (prefers-reduced-motion).

🏎️ 1. Les Principes de la Physique de Ressort (Springs)

Contrairement aux courbes de Bézier cubiques (ease-in-out) qui forcent un temps fixe indépendamment de la distance parcourue, un ressort physique calcule la trajectoire en temps réel :

  • Stiffness (Raideur) : Vitesse de rappel du ressort. Une raideur élevée (300–500) rend le contrôle vif et immédiat.
  • Damping (Amortissement) : Friction qui freine le mouvement. Un amortissement bien réglé (20–30) évite les oscillations oscillatoires excessives (« effet cartoon »).
  • Mass (Masse) : Inertie de l'élément (0.5 à 1.0 par défaut).

Les 3 Profils de Ressort Standards

Profil Cas d'Usage Paramètres Recommandés
Snappy / Micro-tactile Boutons, toggles, badges, chips, icones { type: 'spring', stiffness: 500, damping: 30, mass: 0.5 }
Smooth / Layout Modales, volets latéraux, accordéons, cartes { type: 'spring', stiffness: 350, damping: 28, mass: 0.8 }
Gentle / Page Transition Entrées de page, vues séquentielles { type: 'spring', stiffness: 200, damping: 24, mass: 1.0 }

💫 2. Transitions Partagées avec layoutId (Technique FLIP)

Pour un indicateur d'onglet ou un sous-lignage qui glisse d'un bouton à l'autre sans saut visuel :

import { motion } from 'framer-motion';
import { useState } from 'react';

const tabs = ['Vue d\'ensemble', 'Analytique', 'Paramètres'];

export const TabNavigation = () => {
  const [activeTab, setActiveTab] = useState(tabs[0]);

  return (
    <nav className="flex gap-2 p-1.5 rounded-xl bg-[oklch(0.18_0.01_260)] border border-[oklch(1_0_0/0.08)]">
      {tabs.map((tab) => {
        const isActive = activeTab === tab;
        return (
          <button
            key={tab}
            onClick={() => setActiveTab(tab)}
            className="relative px-4 py-1.5 text-sm font-medium rounded-lg text-white/80 hover:text-white transition-colors"
          >
            {/* Pilule d'arrière-plan animée en FLIP automatique */}
            {isActive && (
              <motion.div
                layoutId="active-indicator"
                transition={{ type: 'spring', stiffness: 400, damping: 30 }}
                className="absolute inset-0 rounded-lg bg-[oklch(0.28_0.02_260)] shadow-sm"
              />
            )}
            <span className="relative z-10">{tab}</span>
          </button>
        );
      })}
    </nav>
  );
};

♿ 3. Respect Impératif de prefers-reduced-motion

Chaque animation doit se désactiver instantanément pour les utilisateurs sensibles au mal des transports ou ayant activé la réduction de mouvement dans leur OS :

import { motion, useReducedMotion } from 'framer-motion';

export const AccessibleCard = ({ children }: { children: React.ReactNode }) => {
  const shouldReduceMotion = useReducedMotion();

  return (
    <motion.div
      initial={shouldReduceMotion ? { opacity: 1, y: 0 } : { opacity: 0, y: 12 }}
      animate={{ opacity: 1, y: 0 }}
      exit={shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: -8 }}
      transition={
        shouldReduceMotion
          ? { duration: 0 }
          : { type: 'spring', stiffness: 300, damping: 25 }
      }
      whileHover={shouldReduceMotion ? {} : { scale: 1.015 }}
      whileTap={shouldReduceMotion ? {} : { scale: 0.985 }}
    >
      {children}
    </motion.div>
  );
};

🎨 4. Alternatives CSS Modernes (Sans Dépendance JS)

Pour les transitions simples au chargement d'éléments, privilégiez @starting-style et la View Transitions API :

/* Entrée fluide pure CSS d'un dialog ou popover */
dialog[open] {
  opacity: 1;
  transform: scale(1);
  transition: opacity 0.2s ease-out, transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), display 0.2s allow-discrete;
}

@starting-style {
  dialog[open] {
    opacity: 0;
    transform: scale(0.96);
  }
}

📋 Checklist de Conformité Motion

  • Aucun mouvement décoratif continu en boucle (pas de floating infini qui distrait).
  • Les micro-interactions au clic utilisent active:scale-[0.98] ou un ressort réactif.
  • useReducedMotion() (ou la media query @media (prefers-reduced-motion: reduce)) ramène la durée à 0.
  • Les animations de changement de layout (layoutId) ne causent aucun reflow de texte brutal.
  • La performance d'animation cible 60fps/120fps en animant exclusivement transform et opacity.