agentleFS
soilmasssoilmass/motion-design-agent/skills/follow-through/SKILL.md

follow-through

Skill7 starsChanged 10 months agoMITView on GitHub

Disney's principle of secondary motion that continues after primary action stops, plus overlapping action for staggered element timing. Use for natural settling, spring physics, and cascading animations.

1 other project uses a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

5 rules. About 0 are common, 5 are distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈5

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Follow-through is best achieved with spring physics:

    line 60
  2. Distinctive:

    Too much overshoot: - Keep under 1.2x for UI

    line 441
  3. Distinctive:

    Fighting springs: - Don't interrupt spring animations

    line 443
  4. Distinctive:

    squash-stretch: - Follow-through often includes diminishing squash oscillations

    line 450
  5. Distinctive:

    arc-motion: - Follow-through motion often continues along arc paths

    line 453

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Follow-Through & Overlapping Action

Secondary elements continue moving after the primary action stops, creating natural, physics-based settling. Overlapping action staggers multiple elements for fluid choreography.

Core Principles

Follow-Through

Secondary elements lag behind and overshoot before settling.

Primary stops:    ################ STOP
Secondary:        ####################-><-> SETTLE
                              ^
                      overshoot + oscillation

Overlapping Action

Multiple elements animate with staggered timing, not simultaneously.

All at once:      ########  (robotic)
                  ########
                  ########

Overlapping:      ########        (natural)
                    ########
                      ########

Token Reference

Load from: tokens/follow-through.tokens.json

Follow-Through Configs

TokenDelayOvershootSettleUse Case
tight30ms1.02100msEnterprise, controlled
loose80ms1.08200msConsumer, friendly
fluid120ms1.15300msPlayful, organic
heavy150ms1.05400msWeighty, dramatic
elastic60ms1.25350msBouncy, rubber-like

Overlap Offsets

TokenOffsetEffect
sequential20%Elements nearly finished before next starts
staggered40%Standard wave effect
cascading60%Multiple elements animating at once
simultaneous80%Near-instant cascade

Spring Physics

Follow-through is best achieved with spring physics:

Spring Parameters:
- stiffness: How quickly it snaps back (higher = faster)
- damping: How quickly oscillations die (lower = bouncier)
- mass: Weight of the element (higher = slower, heavier)

Configs from tokens:
- tight-spring:   stiffness: 400, damping: 30, mass: 1
- standard-spring: stiffness: 200, damping: 20, mass: 1
- bouncy-spring:  stiffness: 300, damping: 10, mass: 1
- wobbly-spring:  stiffness: 180, damping: 8, mass: 1

Implementation Presets

CSS

/* Follow-through with keyframes */
@keyframes follow-through-settle {
  0% { transform: translateX(0); }
  60% { transform: translateX(100px); }
  75% { transform: translateX(108px); } /* Overshoot */
  85% { transform: translateX(97px); }  /* Undershoot */
  95% { transform: translateX(101px); } /* Tiny overshoot */
  100% { transform: translateX(100px); }
}

/* Staggered list items */
.list-item {
  opacity: 0;
  transform: translateY(20px);
  animation: slide-in 300ms ease-out forwards;
}

.list-item:nth-child(1) { animation-delay: 0ms; }
.list-item:nth-child(2) { animation-delay: 60ms; }
.list-item:nth-child(3) { animation-delay: 120ms; }
.list-item:nth-child(4) { animation-delay: 180ms; }

@keyframes slide-in {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* CSS spring approximation using cubic-bezier */
.spring-settle {
  transition: transform 400ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
}

GSAP

import { gsap } from 'gsap';
import tokens from '../tokens/follow-through.tokens.json';

// Follow-through with elastic easing
function animateWithFollowThrough(element, target, intensity = 'loose') {
  const config = tokens['follow-through'][intensity].$value;

  return gsap.to(element, {
    ...target,
    duration: config.settleDuration / 1000 + 0.3,
    ease: `elastic.out(1, ${1 - config.overshoot + 0.5})`
  });
}

// Staggered list animation
function staggerList(items, intensity = 'staggered') {
  const offset = parseFloat(tokens.overlap[intensity].$value.offset) / 100;
  const staggerTime = 0.3 * (1 - offset); // Convert offset to stagger delay

  return gsap.from(items, {
    opacity: 0,
    y: 30,
    duration: 0.4,
    stagger: staggerTime,
    ease: 'back.out(1.7)'
  });
}

// Primary + secondary element follow-through
function primarySecondaryAnimation(primary, secondary, intensity = 'loose') {
  const { delay, overshoot, settleDuration } = tokens['follow-through'][intensity].$value;

  const tl = gsap.timeline();

  // Primary movement
  tl.to(primary, {
    x: 200,
    duration: 0.3,
    ease: 'power2.out'
  });

  // Secondary follows with delay and overshoot
  tl.to(secondary, {
    x: 200 * overshoot,
    duration: 0.3,
    ease: 'power2.out'
  }, `<${delay / 1000}`); // Start slightly after primary

  // Secondary settles back
  tl.to(secondary, {
    x: 200,
    duration: settleDuration / 1000,
    ease: 'elastic.out(1, 0.5)'
  });

  return tl;
}

// Card with badge follow-through
function cardWithBadge(card, badge) {
  const tl = gsap.timeline();

  // Card moves
  tl.to(card, {
    y: -20,
    duration: 0.2,
    ease: 'power2.out'
  });

  // Badge follows with lag and wobble
  tl.to(badge, {
    y: -25, // Overshoots
    rotation: 5,
    duration: 0.25,
    ease: 'power2.out'
  }, '<0.05'); // Slight delay

  // Badge settles
  tl.to(badge, {
    y: -20,
    rotation: 0,
    duration: 0.3,
    ease: 'elastic.out(1, 0.4)'
  });

  return tl;
}

Framer Motion

import { motion, useAnimation, useSpring } from 'framer-motion';
import tokens from '../tokens/follow-through.tokens.json';

// Spring-based follow-through
const springConfigs = {
  tight: { stiffness: 400, damping: 30, mass: 1 },
  loose: { stiffness: 200, damping: 20, mass: 1 },
  bouncy: { stiffness: 300, damping: 10, mass: 1 },
  wobbly: { stiffness: 180, damping: 8, mass: 1 }
};

export const FollowThroughCard: React.FC<{ children: React.ReactNode }> = ({
  children
}) => (
  <motion.div
    whileHover={{ y: -10 }}
    transition={{ type: 'spring', ...springConfigs.loose }}
  >
    {children}
  </motion.div>
);

// Staggered children with overlap
const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.08, // 80ms between children
      delayChildren: 0.1
    }
  }
};

const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: {
    opacity: 1,
    y: 0,
    transition: { type: 'spring', ...springConfigs.loose }
  }
};

export const StaggeredList: React.FC<{ items: string[] }> = ({ items }) => (
  <motion.ul
    variants={containerVariants}
    initial="hidden"
    animate="visible"
  >
    {items.map((item, i) => (
      <motion.li key={i} variants={itemVariants}>
        {item}
      </motion.li>
    ))}
  </motion.ul>
);

// Parent-child follow-through
export const ParentChildFollowThrough: React.FC = () => {
  const controls = useAnimation();

  const handleHover = async () => {
    await controls.start('hover');
  };

  return (
    <motion.div
      className="parent"
      whileHover="hover"
      onHoverStart={handleHover}
    >
      <motion.div
        className="child"
        variants={{
          rest: { y: 0, rotate: 0 },
          hover: {
            y: -15,
            rotate: 5,
            transition: {
              type: 'spring',
              stiffness: 200,
              damping: 15,
              delay: 0.05 // Follow-through delay
            }
          }
        }}
        initial="rest"
      />
    </motion.div>
  );
};

Remotion

import { useCurrentFrame, interpolate, spring } from 'remotion';
import tokens from '../tokens/follow-through.tokens.json';

// Spring-based follow-through
export const useFollowThrough = (
  primaryStopFrame: number,
  config: 'tight' | 'loose' | 'fluid' = 'loose'
) => {
  const frame = useCurrentFrame();
  const { delay, overshoot, settleDuration } = tokens['follow-through'][config].$value;
  const delayFrames = Math.round((delay / 1000) * 30);

  const secondaryFrame = frame - delayFrames;

  // Spring physics for settling
  const springProgress = spring({
    frame: secondaryFrame - primaryStopFrame,
    fps: 30,
    config: {
      damping: 15,
      stiffness: 150,
      mass: 1,
      overshootClamping: false
    }
  });

  return {
    progress: springProgress,
    overshoot: overshoot,
    scale: 1 + (overshoot - 1) * (1 - springProgress),
    rotation: interpolate(springProgress, [0, 0.5, 1], [5, -2, 0])
  };
};

// Staggered list animation
export const StaggeredList: React.FC<{
  startFrame: number;
  items: React.ReactNode[];
  staggerFrames?: number;
}> = ({ startFrame, items, staggerFrames = 3 }) => {
  const frame = useCurrentFrame();

  return (
    <>
      {items.map((item, index) => {
        const itemStartFrame = startFrame + index * staggerFrames;

        const progress = spring({
          frame: frame - itemStartFrame,
          fps: 30,
          config: { damping: 20, stiffness: 200, mass: 1 }
        });

        const opacity = interpolate(progress, [0, 1], [0, 1]);
        const y = interpolate(progress, [0, 1], [30, 0]);

        return (
          <div
            key={index}
            style={{
              opacity,
              transform: `translateY(${y}px)`
            }}
          >
            {item}
          </div>
        );
      })}
    </>
  );
};

// Card with secondary element
export const CardWithBadge: React.FC<{
  moveFrame: number;
  children: React.ReactNode;
}> = ({ moveFrame, children }) => {
  const frame = useCurrentFrame();

  // Primary card spring
  const cardSpring = spring({
    frame: frame - moveFrame,
    fps: 30,
    config: { damping: 20, stiffness: 200 }
  });

  // Secondary badge spring (delayed)
  const badgeSpring = spring({
    frame: frame - moveFrame - 2, // 2 frame delay
    fps: 30,
    config: { damping: 12, stiffness: 180 } // Bouncier
  });

  const cardY = interpolate(cardSpring, [0, 1], [0, -30]);
  const badgeY = interpolate(badgeSpring, [0, 1], [0, -35]); // Overshoots
  const badgeRotation = interpolate(badgeSpring, [0, 0.7, 1], [0, 8, 0]);

  return (
    <div style={{ transform: `translateY(${cardY}px)` }}>
      {children}
      <div
        className="badge"
        style={{
          transform: `translateY(${badgeY - cardY}px) rotate(${badgeRotation}deg)`
        }}
      />
    </div>
  );
};

Secondary Element Hierarchy

Element TypeDelayBehavior
Badges, icons on cards1-2 framesTight, minimal lag
Labels, text2-3 framesSubtle follow
Decorative elements3-5 framesNoticeable lag
Hair, cloth, ribbons5-10 framesPronounced drag
Independent elementsVariesOwn physics

Accessibility

const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

// Reduced motion: remove stagger and settle
const stagger = prefersReducedMotion ? 0 : 0.08;
const transition = prefersReducedMotion
  ? { duration: 0.01 }
  : { type: 'spring', stiffness: 200, damping: 20 };

Common Mistakes

  1. Too much overshoot - Keep under 1.2x for UI
  2. Same timing everywhere - Vary stagger by importance
  3. Fighting springs - Don't interrupt spring animations
  4. No hierarchy - Primary should clearly lead secondaries
  5. Over-staggering - Too much delay feels sluggish

Related Skills

  • anticipation - The wind-up phase that precedes the motion follow-through settles
  • squash-stretch - Follow-through often includes diminishing squash oscillations
  • secondary-action - Secondary actions have their own follow-through behaviors
  • exaggeration - Controls intensity of overshoot and settle oscillations
  • arc-motion - Follow-through motion often continues along arc paths

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.