Framer Motion : Animez vos applications en toute simplicité

par

Lucie

Lucie Ory

14 minutes de lecture

Framer Motion : Animez vos applications en toute simplicité

En matière de design web et mobile, le mouvement est aujourd’hui la norme. Animation à l’activation d’un bouton, interactions liées au scroll, ou navigation entre les pages : les applications prennent vie et deviennent beaucoup plus intuitives et ludiques pour les utilisateurs. Pour ne pas se perdre dans un cauchemar d’animations CSS illisibles, bien s’équiper est essentiel. C’est là que Framer Motion entre en action.

1. Framer Motion, c’est quoi ?

Framer Motion est une librairie d’animation de composants. Cette bibliothèque met à disposition une API simple d’utilisation, permettant de gérer en quelques props :

  • l’animation des composants
  • leur réaction aux mouvements de l’utilisateur
  • les interactions entre les mouvements de différents composants (rythme, priorité d'exécution)
  • les animations à la suppression des composants du DOM

Bref un menu bien complet.

2. Notre Projet

Pour évaluer les capacités de cette bibliothèque, nous allons créer un livre animé. Noël arrive, on va donc retomber en enfance pour un temps avec “Alice au Pays des Merveilles”. Pour ceux qui veulent passer directement à l'animation, le starter kit est disponible en dessous.

C’est parti :

La base de l'app

La base de notre app sera en Next.js

npx create-next-app

On ajoutera aussi Typescript

yarn add --dev typescript @types/react @types/node

_app.js et index.js deviennent du coup _app.tsx et index.tsx

Bien entendu nous avons besoin de framer motion

yarn add framer-motion

Et nous utiliserons également la librairie de composants Chakra Ui pour structurer facilement nos pages. Framer Motion fait d'ailleurs partie des packages obligatoires à l'installation depuis la v1 :

yarn add @chakra-ui/react @emotion/react @emotion/styled

On ajoute le Provider Chakra à notre app :

// _app.tsx
import { ChakraProvider } from "@chakra-ui/react"
...
  return (
    <ChakraProvider>
      <App />
    </ChakraProvider>
  )

Le contenu de l’app

Il nous faut aussi récupérer les deux premiers chapitres du texte de notre livre.

Chaque chapitre sera composé :

  • d’une illustration principale pour la page d’accueil
  • d’un titre et sous titre
  • et de pages contenant elle même des Paragraphes et possiblement une animation à afficher en haut ou en bas de la page

Notre app a donc besoin de 3 composants principaux :

  • un composant Chapter qui affiche les chapitres du livre
  • un composant Page qui affiche les paragraphes de chaque page du chapitre
  • un composant BookNav qui permet de naviguer entre les pages et les chapitres

Starter Kit

Une fois tout ça mis en place, on a notre app prête à animer : STARTER KIT

Pour démarrer le projet :

yarn dev

3. A l’origine du mouvement : le composant Motion

La doc officielle sur Motion

Tout mouvement avec Framer Motion repose sur un composant motion. Il existe un composant motion pour chaque élément HTML ainsi que pour les SVG .

<motion.div></motion.div>
<motion.h1></motion.h1>

Ce composant accepte deux propriétés :

  • initial : l’état du composant avant l’animation - son aspect d’origine
  • animate : l’état du composant après animation - son aspect final

Pour sélectionner le type de transformation à appliquer on passe à initial et animate un objet contenant les propriétés css que l’on souhaite manipuler. Framer Motion met également à disposition des propriétés supplémentaires pour faciliter les transformations :

  • x et y : la position horizontale et verticale de l’élément sur la page, 0 étant sa position naturelle
  • scale et rotate : pour l’échelle et la rotation de l’élément

Commençons par une animation simple du titre du livre sur index.tsx avec un composant <motion.div> :

// index.tsx
import { motion } from 'framer-motion'
...
     <motion.div
       initial={{ opacity: 0, y: '-100px' }}
       animate={{ opacity: 1, y: 0 }}
     >
       <Text textAlign="center" fontSize="3xl" p={4}>
         {book.title}
       </Text>
     </motion.div>

Sur la page d’accueil le titre du livre va maintenant automatiquement se déplacer horizontalement de haut en bas, et augmenter progressivement sa visibilité. Chakra Ui, de son côté, offre aussi la possibilité de combiner la variété des animations framer motion avec la simplicité de l'intégration de ses composants modulaires en 1 ligne seulement :

import { Box } from '@chakra-ui/react';
import { motion } from 'framer-motion';

// 1. Create a custom motion component from Box
const MotionBox = motion.custom(Box);

// 2. You'll get access to `motion` and `chakra` props in `MotionBox`
function Example() {
  return (
    <MotionBox
      boxSize="40px"
      bg="red.300"
      drag="x"
      dragConstraints={{ left: -100, right: 100 }}
      whileHover={{ scale: 1.1 }}
      whileTap={{ scale: 0.9 }}
    />
  );
}

Et voilà! Un composant au style totalement modulable, agrémenté de nouvelles props pour l'animation. Pour ce premier tutoriel, nous conserverons la notation <motion.div> pour mieux nous retrouver dans la doc officielle. Mais les applications de cette combinaison Chakra Ui + framer motion sont très prometteuses.

Notre animation fonctionne mais elle est trop rapide, nous avons besoin d’un mouvement plus lent. C’est là qu’intervient la propriété transition.

4. Transition : Régler plus finement le type d’animation sur nos composants

La doc officielle sur transition

La propriété transition permet de préciser au composant motion la façon dont il doit gérer l’animation. Comme initial et animate il accepte un objet pour paramétrer la transition. Les propriétés de cet objet sont appelées des propriétés d'orchestration. Nous allons essentiellement utiliser :

  • delay : le nombre de secondes à attendre avant de lancer l’animation
  • duration : le nombre de secondes sur lesquelles l’animation va se dérouler
  • repeat : combien de fois répéter l’animation (un chiffre ou Infinity pour un mouvement perpétuel)
  • repeatType : "loop" | "reverse" | "mirror" la façon dont on répètera l’animation

Notre animation étant trop rapide nous pouvons lui passer une propriété duration de 1.5 pour la ralentir :

// index.tsx

<motion.div initial={{ opacity: 0, y: '-100px' }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 1.5 }}>
  <Text textAlign="center" fontSize="3xl" p={4}>
    {book.title}
  </Text>
</motion.div>

Le mouvement est maintenant plus fluide :

Transition se base sur 3 types de mouvements pour réaliser les animations :

  • spring : qui anime l’élément en le faisant légèrement rebondir
  • tween : qui utilise un mouvement plus uniforme
  • intertia : basé sur un mouvement qui décélère progressivement

Les types de transitions par défaut

Transition accepte un paramètre type pour spécifier le type de mouvement à appliquer. Cependant certaines propriétés d’animations définies sur animate et initial induiront certains mouvements par défaut :

  • les modifications des propriétés physiques X et Y utilisent spring par défaut
  • les changements d’opacité ou de couleur passent eux par tween

Chaque type d’animation acceptera des paramètres d’orchestration différents. Les possibilités de combinaisons sont infinies :

Testons les possibilités avec spring. Quoi de mieux qu’un lapin pour bondir ? Nous allons donner du mouvement à l’illustration du lapin visible sur /preview :

// BouncingBunny.tsx

import { motion } from 'framer-motion';

const BoucingBunny = () => {
  return (
    <Center p={10}>
      <motion.div
        initial={{ y: '-5vh' }}
        animate={{ y: 0 }}
        transition={{
          type: 'spring',
          repeat: Infinity,
          duration: 2,
          bounce: 0.7,
          repeatType: 'reverse',
        }}
      >
        <Image src="/alice/bunny.png" maxH="200px" />
      </motion.div>
    </Center>
  );
};

Ici on a fait varier bounce qui représente l'élasticité de notre composant par défaut à 0.25. Le lapin rebondira d’autant plus que cette valeur sera élevée. L’animation boucle grâce à repeat: Infinity. Et le mouvement est plus naturel en passant le repeatType à reverse car le point d’origine de l’animation s’adapte alors à chaque tour.

Il existe un grand nombre de paramètres d’orchestration applicables à spring. On peut régler en fonction des besoins la raideur du mouvement (stiffness) son amortissement (damping) ou la masse de l’objet (mass) pour faire varier l’aspect du rebond.

5. Keyframes : Créer une animation progressive

La doc officielle sur keyFrames

Bien, nos animations font maintenant passer nos composants d’un état à l’autre, en suivant des paramètres de transition spécifiques. Mais comment fait-on quand on a besoin d’animations plus complexes ? si par exemple nous avons besoin de donner des états intermédiaires à notre composant entre la valeur initiale et la valeur finale ?

Au lieu de spécifier une valeur simple à une propriété de animate on peut passer un array de valeurs. Le composant passera alors par tous ces états au fil de l’animation. Essayons les Keyframes sur une nouvelle animation, AliceFalling.tsx :

// AliceFalling.tsx

import { motion } from 'framer-motion';

const fallingVariant = {
  visible: {
    rotate: [-100, 100, -10, 120, 80, 360],
    transition: {
      ease: 'easeInOut',
      duration: 5,
      repeat: 3,
      repeatType: 'reverse',
    },
  },
};

const AliceFalling = () => {
  return (
    <Center p={10}>
      <motion.div variants={fallingVariant} animate="visible">
        <Image src="/alice/aliceFalling.png" maxH="200px" />
      </motion.div>
    </Center>
  );
};

Les Keyframes alternants font évoluer le mouvement du personnage de manière beaucoup moins linéaire qu’un simple animate.

Les keyframes sont par défaut espacés régulièrement au long de l’animation. Si on souhaite une exécution moins linéaire, on peut passer à transition, une propriété times associée à un array de valeur entre 0 et 1 de la même longueur que celui des keyframes. Chaque valeur de times correspondra alors au moment où l’animation le keyframe devra s’activer.

6. Variants : Structurer et refactoriser nos animations

La doc officielle sur variants

Nous savons maintenant appliquer des animations à nos composants. Mais passer systématiquement toutes les propriétés transition / initial / animate à chaque composant alourdit visuellement beaucoup notre code. Ce n'est pas une solution très optimisée :

  • On ne peut pas réutiliser le même effet sur plusieurs composants
  • Il est impossible de contrôler le déroulement d’animations imbriquées
  • Enfin nous ne pouvons pas utiliser des paramètres dynamiques pour les animations

Bref pas très pratique dès qu’on commence à avoir besoin de plusieurs animations ou de mouvements plus complexes...

Les Variants à la rescousse!

Une variant permet d’extraire la logique d’animation du composant dans une constante. On passe ensuite directement la variant au composant motion et on lui indique le nom des propriétés qui correspondent à son état initial ou animé. Les transitions sont alors incluses dans l’objet correspondant à l'état animate (ici visible) et s’appliqueront automatiquement. Plus besoin de spécifier cette prop séparemment.

Créons une nouvelle animation pour tester les variants dans KeyOnDoor.tsx (visible sur /preview) :

// KeyOnDoor.tsx

import { motion } from 'framer-motion';

const keyVariant = {
  hidden: {
    y: '50px',
    opacity: 0,
    rotate: 0,
  },
  visible: {
    y: '-100px',
    opacity: 1,
    rotate: 180,
    transition: {
      duration: 6,
      repeat: Infinity,
      repeatType: 'mirror',
    },
  },
};

const KeyOnDoor = () => {
  return (
    <Center p={12} flexDirection="column">
      <Image src="/alice/porte.png" maxH="200px" />
      <motion.div variants={keyVariant} animate="visible" initial="hidden">
        <Image src="/alice/cle.png" maxH="100px" />
      </motion.div>
    </Center>
  );
};

Animation parent Animations enfants - propagation et orchestration

Dans le cas d’animations imbriquées, on peut même se passer de spécifier au composant motion enfant le nom de ses propriétés initial et animate, elles seront automatiquement transmises depuis l’animation parent.

Par défaut, toutes les animations se lancent au même moment. Mais en utilisant les variants on a accès à des propriétés de transition bonus pour contrôler l’execution des animations enfant :

  • when : beforeChildren | afterChildren = quand l’animation du composant doit-elle avoir lieu ? Avant ou après l'exécution des animations des composants enfants?
  • delayChildren : le temps en secondes avant de lancer les animations enfants
  • staggerChildren : (stagger = échelonner) le temps de délais en secondes à appliquer entre chaque exécution d’animations enfants

Pour le moment l’affichage de nos pages et de nos chapitres n’est pas très attrayant. Essayons d’imbriquer des animations sur ces deux composants :

  • au chargement du chapitre le titre et le sous-titre glisseront par le haut
  • à chaque changement de page le texte apparaîtra par opacité progressive
  • le titre et le sous titre du chapitre doivent s’afficher avant le contenu de la page

On commence par le composant Page.tsx, avec notre animation enfant. Ici plus besoin de spécifier initial et animate sur le composant motion, nous les définirons sur l’animation parent. La variant suffit donc :

// Page.tsx

import { motion } from 'framer-motion'

const pageVariant = {
 visible: {
   opacity: 1,
   transition: {
     duration: 1,
   },
 },
 hidden: { opacity: 0 },
}

const Page = ({ animation, paragraphs, type }: IPage) => {
 const AnimationComponent = animation?.component
 return (
   <Box minH="70vh">
     <motion.div variants={pageVariant}>

Sur Chapter.tsx maintenant, l'animation parent :

// Chapter.tsx
import { motion } from 'framer-motion';

const chapterVariant = {
  visible: {
    y: 0,
    transition: {
      duration: 1,
      when: 'beforeChildren',
    },
  },
  hidden: { y: '-20vh' },
};

beforeChildren s’assurera que l’animation du titre du chapitre est bien terminée avant de lancer les animations enfants des Pages pour l’affichage du texte. On ajoute le composant motion et on précise bien ici les props variants / initial / animate

// Chapter.tsx
return (
   <Box paddingBottom={20}>
     <motion.div variants={chapterVariant} initial="hidden" animate="visible">

6. Gestures : Réagir aux interactions de l’utilisateur

Nos animations commencent à être plus complexes, mais elles ne réagissent pas encore au comportement de l’utilisateur. Le composant motion met à disposition deux propriétés utiles pour interagir avec les mouvements :

  • whileHover : qui se déclenche quand le pointer passe par dessus le composant
  • whileTap : qui s’active quand l’utilisateur presse et relache le composant

Appliquons une animation au hover et au tap sur les cartes de la page d’accueil sur index.tsx. Nous avions précédemment ajouté une animation sur le titre du livre sur cette même page. Maintenant que nous avons vu les variants, on peut extraire l’animation du titre et utiliser les effets d’orchestration parent/enfant dont nous avions parlé :

  • when:”beforeChildren” pour afficher le titre avant les cartes
  • staggerChildren:1 pour afficher chaque carte l’une après l’autre

Il suffit ensuite de créer l’animation autour de chaque carte et d’ajouter whileHover et un whileTap au composant motion pour obtenir l’effet de rebond au hover et d’opacité au clic :

//index.tsx
const homePageWrapperVariant = {
 hidden: { opacity: 0, y: '-100px' },
 visible: {
   opacity: 1,
   y: 0,
   transition: { duration: 1.5, when: 'beforeChildren', staggerChildren: 1 },
 },
}

const chapterCardVariant = {
 hidden: {
   opacity: 0,
 },
 visible: {
   opacity: 1,
   transition: {
     duration: 1.5,
   },
 },
 hover: {
   scale: 1.1,
   transition: { type: 'spring', bounce: 0.6 },
 },
 tap: {
   opacity: 0.4,
 },
}

export default function AliceFramerMotion() {
 return (
   <Center height="100vh" flexDirection="column">
     <Head>
       <title>Alice Framer Motion App</title>
       <link rel="icon" href="/favicon.ico" />
     </Head>
     <motion.div
       variants={homePageWrapperVariant}
       initial="hidden"
       animate="visible"
     >
       <Text textAlign="center" fontSize="3xl" p={4}>
         {book.title}
       </Text>
       <Flex paddingY={10}>
         {book.chapters.map((chapter, index) => (
           <Box p={4} key={index}>
             <motion.div whileHover="hover" variants={chapterCardVariant}>
               <Link href={`/chapter/${index + 1}`}>
                 <Square

Le résultat sur l'animation :

7. AnimatePresence : Créer des animations de sortie

Une autre fonctionnalité intéressante de Framer Motion permet d’effectuer une animation spécifique au moment où les composants sont supprimés du DOM. On utilise pour ça un nouveau composant <AnimatePresence>. On peut alors ajouter à notre composant motion une prop “exit” et spécifier dans la variant un comportement d’animation pour notre composant avant sa suppression.

Voyons cela en pratique avec une dernière animation, DrinkMe.tsx. Il s’agit ici d’exprimer l’explosion de saveurs de la potion décrite par Alice. Nous voulons donc effectuer les animations suivantes :

  • au chargement du composant une bouteille apparaît
  • au clic sur buvez moi le composant de la bouteille est supprimé
  • avant qu’il soit supprimé on lance les animations de sortie de ses enfants, les icônes d’aliments, qui devront partir dans des directions différentes
  • on lance enfin l’animation de sortie de la bouteille
  • le bouton permet de relancer l’animation

On commence par définir la variant pour la bouteille:

  • exit représente notre animation de sortie pour la bouteille
  • staggerChildren précise le timing d'échelonnement pour les animations de sortie de chaque enfant
  • when:”afterChildren” nous assure que l’animation de sortie de la bouteille ne sera pas lancée avant les enfants
//DrinkMe.tsx
const bottleVariant = {
  hidden: {
    opacity: 0,
    y: 0,
  },
  visible: {
    opacity: 1,
    transition: {
      duration: 1,
    },
  },
  exit: {
    y: ['-30px', '100px'],
    opacity: 0,
    transition: {
      duration: 1,
      when: 'afterChildren',
      staggerChildren: 0.8,
    },
  },
};

Les variants dynamiques

On passe ensuite à la variant pour les enfants. Sachant que chaque icône devra partir dans un sens différent, on tire profit de la possibilité de passer dynamiquement une valeur aux animations par le composant motion (ici la direction d’animation pour x) :

const foodVariant = {
  visible: {
    opacity: 0,
    y: -50,
    position: 'absolute',
  },
  exit: xDirection => ({
    opacity: [1, 0],
    y: -200,
    x: xDirection,
    rotate: 360,
    transition: {
      duration: 2,
    },
  }),
};

On peut maintenant définir nos composants motion et ajouter AnimatePresence :

  • chaque motion.div représentant une icône de nourriture possède alors une prop custom par laquelle nous pouvons passer la direction x à appliquer à l’animation de sortie sur notre variant
  • AnimatePresence dispose d’un callback nous permettant de désactiver le bouton pour relancer l’animation tant que toutes les animations de sortie n’ont pas été jouées
<AnimatePresence onExitComplete={() => setDisabledBtn(false)}>
  {!isPotionEmpty && (
    <motion.div variants={bottleVariant} animate="visible" initial="hidden" exit="exit">
      <motion.div variants={foodVariant} initial={false} custom={-50}>
        <Image src="/alice/candy.png" h="80px" />
      </motion.div>
      <motion.div variants={foodVariant} initial={false} custom={100}>
        <Image src="/alice/pineapple.png" h="80px" />
      </motion.div>
      <motion.div variants={foodVariant} initial={false} custom={-100}>
        <Image src="/alice/dinde.png" h="80px" />
      </motion.div>
      <motion.div variants={foodVariant} initial={false} custom={50}>
        <Image src="/alice/cake-slice.png" h="80px" />
      </motion.div>
      <Image src="/alice/potion.png" h="100px" />
    </motion.div>
  )}
</AnimatePresence>

On obtient le résultat suivant :

MIAM !

Voilà vous avez maintenant un premier aperçu des capacités de Framer Motion qui est un outil de gestion d’animation puissant pour vos composants. Ce premier tour ne couvre qu’une partie des fonctionnalités de l’API, car vous l’aurez compris les possibilités sont infinies.

J’espère que vous avez passé un bon moment à donner vie à vos composants, et que ça vous a donné envie d’en savoir plus sur cette super librairie. Nous verrons dans un prochain article d’autres fonctionnalités intéressantes comme la gestion du scroll, la détection du drag des éléments, les paths animés, ou les animations coordonnées de layout avec AnimateSharedLayout.

Encore tout un programme à découvrir!

Continuer la discussion sur Twitter

18 avenue Parmentier
75011 Paris
+33 1 43 57 39 11
hello@premieroctet.com

Suivez nos aventures

GitHub

Naviguez à vue