interaction-design

Par wshobson · agents

Concevez et implémentez des microinteractions, du motion design, des transitions et des patterns de feedback utilisateur. À utiliser pour peaufiner les interactions UI, implémenter des états de chargement ou créer des expériences utilisateur engageantes.

npx skills add https://github.com/wshobson/agents --skill interaction-design

Design d'Interaction

Créez des interactions engageantes et intuitives grâce au mouvement, aux retours visuels et aux transitions d'état réfléchies qui améliorent l'utilisabilité et ravissent les utilisateurs.

Quand Utiliser cette Skill

  • Ajouter des microinteractions pour améliorer les retours utilisateur
  • Implémenter des transitions fluides de pages et de composants
  • Concevoir des états de chargement et des skeleton screens
  • Créer des interactions basées sur les gestes
  • Construire des systèmes de notifications et de toasts
  • Implémenter des interfaces drag-and-drop
  • Ajouter des animations déclenchées au scroll
  • Concevoir les états hover et focus

Principes Fondamentaux

1. Mouvement Intentionnel

Le mouvement doit communiquer, non décorer :

  • Retour : Confirmer que les actions de l'utilisateur ont eu lieu
  • Orientation : Montrer d'où viennent les éléments et où ils vont
  • Focus : Diriger l'attention vers les changements importants
  • Continuité : Maintenir le contexte lors des transitions

2. Directives de Timing

Durée Cas d'Usage
100-150ms Micro-retours (survols, clics)
200-300ms Petites transitions (bascules, menus)
300-500ms Transitions moyennes (modales, pages)
500ms+ Animations chorégraphiées complexes

3. Fonctions d'Easing

/* Easings courants */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* Décélération - entrée */
--ease-in: cubic-bezier(0.55, 0, 1, 0.45); /* Accélération - sortie */
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* Les deux - déplacement */
--spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* Dépassement - ludique */

Démarrage Rapide : Microinteraction sur Bouton

import { motion } from "framer-motion";

export function InteractiveButton({ children, onClick }) {
  return (
    <motion.button
      onClick={onClick}
      whileHover={{ scale: 1.02 }}
      whileTap={{ scale: 0.98 }}
      transition={{ type: "spring", stiffness: 400, damping: 17 }}
      className="px-4 py-2 bg-blue-600 text-white rounded-lg"
    >
      {children}
    </motion.button>
  );
}

Patterns d'Interaction

1. États de Chargement

Skeleton Screens : Préserver la mise en page pendant le chargement

function CardSkeleton() {
  return (
    <div className="animate-pulse">
      <div className="h-48 bg-gray-200 rounded-lg" />
      <div className="mt-4 h-4 bg-gray-200 rounded w-3/4" />
      <div className="mt-2 h-4 bg-gray-200 rounded w-1/2" />
    </div>
  );
}

Indicateurs de Progression : Afficher la progression déterminée

function ProgressBar({ progress }: { progress: number }) {
  return (
    <div className="h-2 bg-gray-200 rounded-full overflow-hidden">
      <motion.div
        className="h-full bg-blue-600"
        initial={{ width: 0 }}
        animate={{ width: `${progress}%` }}
        transition={{ ease: "easeOut" }}
      />
    </div>
  );
}

2. Transitions d'État

Bascule avec transition fluide :

function Toggle({ checked, onChange }) {
  return (
    <button
      role="switch"
      aria-checked={checked}
      onClick={() => onChange(!checked)}
      className={`
        relative w-12 h-6 rounded-full transition-colors duration-200
        ${checked ? "bg-blue-600" : "bg-gray-300"}
      `}
    >
      <motion.span
        className="absolute top-1 left-1 w-4 h-4 bg-white rounded-full shadow"
        animate={{ x: checked ? 24 : 0 }}
        transition={{ type: "spring", stiffness: 500, damping: 30 }}
      />
    </button>
  );
}

3. Transitions de Pages

Animations de layout avec Framer Motion :

import { AnimatePresence, motion } from "framer-motion";

function PageTransition({ children, key }) {
  return (
    <AnimatePresence mode="wait">
      <motion.div
        key={key}
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        exit={{ opacity: 0, y: -20 }}
        transition={{ duration: 0.3 }}
      >
        {children}
      </motion.div>
    </AnimatePresence>
  );
}

4. Patterns de Retour

Effet d'ondulation au clic :

function RippleButton({ children, onClick }) {
  const [ripples, setRipples] = useState([]);

  const handleClick = (e) => {
    const rect = e.currentTarget.getBoundingClientRect();
    const ripple = {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top,
      id: Date.now(),
    };
    setRipples((prev) => [...prev, ripple]);
    setTimeout(() => {
      setRipples((prev) => prev.filter((r) => r.id !== ripple.id));
    }, 600);
    onClick?.(e);
  };

  return (
    <button onClick={handleClick} className="relative overflow-hidden">
      {children}
      {ripples.map((ripple) => (
        <span
          key={ripple.id}
          className="absolute bg-white/30 rounded-full animate-ripple"
          style={{ left: ripple.x, top: ripple.y }}
        />
      ))}
    </button>
  );
}

5. Interactions Gestuelles

Glisser pour rejeter :

function SwipeCard({ children, onDismiss }) {
  return (
    <motion.div
      drag="x"
      dragConstraints={{ left: 0, right: 0 }}
      onDragEnd={(_, info) => {
        if (Math.abs(info.offset.x) > 100) {
          onDismiss();
        }
      }}
      className="cursor-grab active:cursor-grabbing"
    >
      {children}
    </motion.div>
  );
}

Patterns d'Animation CSS

Animations Keyframe

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

.animate-fadeIn {
  animation: fadeIn 0.3s ease-out;
}
.animate-pulse {
  animation: pulse 2s ease-in-out infinite;
}
.animate-spin {
  animation: spin 1s linear infinite;
}

Transitions CSS

.card {
  transition:
    transform 0.2s ease-out,
    box-shadow 0.2s ease-out;
}

.card:hover {
  transform: translateY(-4px);
  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
}

Considérations d'Accessibilité

/* Respecter les préférences de mouvement utilisateur */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
function AnimatedComponent() {
  const prefersReducedMotion = window.matchMedia(
    "(prefers-reduced-motion: reduce)",
  ).matches;

  return (
    <motion.div
      animate={{ opacity: 1 }}
      transition={{ duration: prefersReducedMotion ? 0 : 0.3 }}
    />
  );
}

Bonnes Pratiques

  1. Performance d'Abord : Utiliser transform et opacity pour du 60fps fluide
  2. Support Reduced Motion : Toujours respecter prefers-reduced-motion
  3. Timing Cohérent : Utiliser une échelle de timing dans toute l'app
  4. Physique Naturelle : Préférer les animations spring aux animations linéaires
  5. Interruptible : Permettre aux utilisateurs d'annuler les longues animations
  6. Amélioration Progressive : Fonctionner sans animations JS
  7. Tester sur Appareils : La performance varie significativement

Problèmes Courants

  • Animations Saccadées : Éviter d'animer width, height, top, left
  • Sur-animation : Trop de mouvement cause la fatigue
  • Interactions Bloquées : Ne jamais empêcher l'input utilisateur durant les animations
  • Fuites Mémoire : Nettoyer les listeners d'animation au unmount
  • Flash de Contenu : Utiliser will-change avec parcimonie pour l'optimisation

Skills similaires