Creating Primitive Motion Design System Hooks Using Framer Motion

Design systems have been recognized as extremely important for harmonizing the look, feel, and behavior of various applications within a company.

However, we don't always see such an organized system for stitching together consistent, meaningful animations.

Quite simply, we should consider bringing motion into our design systems.

The Difficulty

Bringing motion into a design system isn't as easy as it sounds for the following reasons:

  1. Managing a design system without motion is enough work in and of itself.

  2. Orchestrating motions is a unique skillset.

  3. Without motion expertise, it's a bit easier to say what we don't want instead of what we do want.

  4. Design specifications can be grouped by component. It's harder to generalize motion "groups" as it may vary depending on the context.

In a word, it's not always obvious how to generalize animations across a stack of applications, and even if some trends are obvious, it can be time-consuming.

A Possible Solution

A potential solution for reducing the cognitive load of managing a motion design system is to focus on the primitives.

Instead of trying to generalize animations for shared contexts across various applications, focus on organizing the primitives of a motion design system.

What are the primitives?

The primitives of motion design system are:

  1. Animation types
  2. Easings and durations based on type
  3. Basic motions

By defining these primitives, we can organize them into a system and expose assets to apply that system in code.

Animation Types

Generally, you can group an animation into 3 types:

  1. Entrance - Animating an object when it enters
  2. Exit - Animating an object when it exits
  3. Effect - Animating an object that has already entered but is not exiting
// motion.js
const types = {
  entrance: 'entrance',
  exit: 'exit',
  effect: 'effect',
};

Easings and Durations

Duration refers to how long it takes to animate a property from point a to point b (i.e. 200ms, 250ms, 500ms).

// motion.js
const types = {
  entrance: 'entrance',
  exit: 'exit',
  effect: 'effect',
};

const durations = {
  fast: 200,
  slow: 250,
};

Easing refers to where you animate most of the property in the animation's timeline (from point a to point b).

easeOut is an easing function used primarily for entrance (the opposite of "out") animations.

It animates most of the property of the its way "out".

easeIn is an easing function used primarily for exit (the opposite of "in") animations.

It animates most of the property of the its way "in".

easeInOut is an easing function that animates most of the property in the middle of the timeline.

It is used primarily for animating something that is neither entering nor exiting.

// motion.js
const types = {
  entrance: 'entrance',
  exit: 'exit',
  effect: 'effect',
};

const durations = {
  fast: 200,
  slow: 250,
};

const easings = {
  effect: 'easeInOut',
  entrance: 'easeOut',
  exit: 'easeIn',
};

Putting it all together, we can map a duration and easing function to a motion type:

// motion.js
const types = {
  entrance: 'entrance',
  exit: 'exit',
  effect: 'effect',
};

const durations = {
  effect: 250,
  entrance: 250,
  exit: 250,
};

const easings = {
  effect: 'easeInOut',
  entrance: 'easeOut',
  exit: 'easeIn',
};

const transitions = {
  effect: {
    duration: durations[types.effect],
    ease: easings[types.effect],
  },
  entrance: {
    duration: durations[types.entrance],
    ease: easings[types.entrance],
  },
  exit: {
    duration: durations[types.exit],
    ease: easings[types.exit],
  },
};

Basic Motions

Finally, we can call out common, basic types of motions and map to their transition type:

// motion.js
const types = {
  entrance: 'entrance',
  exit: 'exit',
  effect: 'effect',
};

const durations = {
  effect: 250,
  entrance: 250,
  exit: 250,
};

const easings = {
  effect: 'easeInOut',
  entrance: 'easeOut',
  exit: 'easeIn',
};

const transitions = {
  effect: {
    duration: durations[types.effect],
    ease: easings[types.effect],
  },
  entrance: {
    duration: durations[types.entrance],
    ease: easings[types.entrance],
  },
  exit: {
    duration: durations[types.exit],
    ease: easings[types.exit],
  },
};

const motions = {
  move: { transition: transitions[types.effect] },
  moveIn: { transition: transitions[types.entrance] },
  moveOut: { transition: transitions[types.exit] },
  // ...etc
};

Exposing the Motions Via Hooks (Using Framer Motion)

Once the basic motions have been grouped by type, and the types have been mapped to a common duration and ease, we can export these to work with specific technologies/frameworks.

Here's an example if of exposing a basic motion hook by wrapping Framer Motion:

// motion.js

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

// ...

function toSeconds({ ms }) {
  return ms / 1000;
}

function normalize(transition) {
  return {
    ...transition,
    duration: toSeconds({ ms: transition.duration }),
  };
}

export function useMove(config = {}) {
  const controls = useAnimation();
  return {
    motion,
    animate: controls,
    trigger: (animatedProperties = {}) => {
      controls.start({
        ...animatedProperties,
        transition: normalize(transitions.move),
      });
    },
  };
};

// SomeComponent.jsx
import React, { useState } from 'react';
import { useMove } from '...';

const SomeComponent = () => {
  const [isShifted, setIsShifted] = useState();
  const { motion, animate, trigger } = useMove();
  return (
    <motion.div
     animate={animate}
     onClick={() => {
       trigger({ x: isShifted ? 0 : 100 });
       setIsShifted(!isShifted);
     }}
    >
      Click me!
    </motion.div>
  );
};

🎉 Tada! We've established a way to organize our motions in code.

Now, you may want to approach this quite differently, but my hope is that this gets the conversation going.

24