Repeat & Yoyo

Loop animations with repeat count and yoyo back-and-forth playback.

repeat controls how many times an animation cycles after its first play. Combine it with yoyo to reverse direction on alternating cycles, and delay to add a pause between each cycle.

Basic Repeat

Pass repeat as a plain number to run the animation that many additional times after the first play. A value of 2 plays 3 times total.

typescript
import { Motion } from "@motion.page/sdk";

Motion("bounce", ".ball", {
  to: { y: -40 },
  duration: 0.4,
  ease: "power2.out",
  repeat: 2,
}).onPageLoad();

Infinite Loop

Use repeat: -1 to loop forever.

typescript
Motion("spin", ".loader", {
  to: { rotate: 360 },
  duration: 1,
  ease: "none",
  repeat: -1,
}).onPageLoad();

RepeatConfig Object

For more control, pass an object with times, yoyo, and delay properties.

PropertyTypeDefaultDescription
timesnumberAdditional repetitions. -1 = infinite
yoyobooleanfalseReverse direction on every other cycle
delaynumber0Seconds to pause between each cycle
typescript
// Shorthand number
repeat: 3

// Full config object
repeat: { times: 3, yoyo: true, delay: 0.5 }

Yoyo

yoyo: true reverses the animation on every alternate cycle — forward, backward, forward, backward. This creates smooth back-and-forth playback without a hard reset jump.

typescript
Motion("pulse", ".badge", {
  to: { scale: 1.15 },
  duration: 0.6,
  ease: "power1.inOut",
  repeat: { times: -1, yoyo: true },
}).onPageLoad();

Without yoyo, an infinite repeat jumps back to the start after each cycle. With yoyo, the animation reverses smoothly — ideal for breathing, pulsing, and attention effects.

Repeat Delay

delay inside RepeatConfig adds a pause between cycles — distinct from the top-level delay which only delays the very first play.

typescript
Motion("blink", ".cursor", {
  to: { opacity: 0 },
  duration: 0.5,
  ease: "power1.inOut",
  repeat: { times: -1, yoyo: true, delay: 0.1 },
}).onPageLoad();

This lives inside the RepeatConfig object as delay.

Finite Repeat with Yoyo

Combine a finite times count with yoyo for animations that play a set number of back-and-forth swings, then settle.

typescript
Motion("shake", ".notification", {
  to: { x: 8 },
  duration: 0.08,
  ease: "power1.inOut",
  repeat: { times: 5, yoyo: true },
}).play();

5 additional cycles with yoyo = left, right, left, right, left, right — a natural shake effect.

Complete Example — All Three Properties

typescript
import { Motion } from "@motion.page/sdk";

Motion("breathe", ".hero-icon", {
  to: { scale: 1.08, opacity: 0.85 },
  duration: 1.8,
  ease: "power1.inOut",
  repeat: { times: -1, yoyo: true, delay: 0.3 },
}).onPageLoad();

The icon gently expands and dims, holds for 0.3s, then reverses — repeating indefinitely.

Timeline Repeat with .withRepeat()

repeat inside AnimationConfig loops a single animation. To loop an entire multi-step timeline, use the .withRepeat() method on the Timeline instance instead.

typescript
// Per-animation repeat — only the individual tween loops
Motion("slide", ".box", {
  from: { x: -100 },
  to: { x: 100 },
  duration: 0.8,
  repeat: -1,
}).onPageLoad();

// Timeline repeat — the whole sequence loops as a group
Motion("sequence", [
  { target: ".box-a", from: { x: -100 }, duration: 0.5 },
  { target: ".box-b", from: { y: 60 }, duration: 0.5 },
])
  .withRepeat({ times: -1, yoyo: true })
  .onPageLoad();

.withRepeat() accepts the same number | RepeatConfig as per-animation repeat.

onRepeat Callback

React to each completed cycle with the onRepeat callback. It receives the current repeat count (0-based).

typescript
Motion("counter", ".ring", {
  to: { rotate: 360 },
  duration: 1,
  ease: "none",
  repeat: -1,
  onRepeat: (count) => {
    console.log(`Cycle ${count + 1} complete`);
  },
}).onPageLoad();

Common Patterns

Attention pulse — draw focus to a CTA or notification badge:

typescript
Motion("cta-pulse", ".cta-button", {
  to: { scale: 1.06, boxShadow: "0 0 0 8px rgba(102, 51, 238, 0.25)" },
  duration: 0.8,
  ease: "power1.inOut",
  repeat: { times: -1, yoyo: true },
}).onPageLoad();

Loading spinner — continuous rotation with no easing for constant speed:

typescript
Motion("spinner", ".loader-icon", {
  to: { rotate: 360 },
  duration: 0.9,
  ease: "none",
  repeat: -1,
}).onPageLoad();

Looping marquee position — translate then instantly reset:

typescript
Motion("marquee", ".track", {
  to: { x: "-50%" },
  duration: 8,
  ease: "none",
  repeat: -1,
}).onPageLoad();

Shake on error — finite yoyo for form validation feedback:

typescript
Motion("field-shake", ".input-error", {
  to: { x: 6 },
  duration: 0.07,
  ease: "power1.inOut",
  repeat: { times: 7, yoyo: true },
}).play();

API Reference

typescript
// Shorthand
repeat?: number;               // -1 = infinite, 3 = 3 extra cycles

// Full config
interface RepeatConfig {
  times:   number;             // Additional repetitions; -1 = infinite
  yoyo?:   boolean;            // Reverse direction each cycle (default: false)
  delay?:  number;             // Seconds to pause between cycles (default: 0)
}

// Timeline-level repeat
tl.withRepeat(config: number | RepeatConfig): this

// Callback on each completed cycle
onRepeat?: (repeatCount: number) => void;

Common Mistakes

Confusing top-level delay with RepeatConfig.delay. delay: 0.5 at the config root delays the first play only. repeat: { times: -1, delay: 0.5 } pauses 0.5s between every cycle. Both can coexist.

Expecting repeat: 3 to play 3 times total. It plays 3 additional times after the first — 4 plays total. Use repeat: 2 for 3 total plays.

Forgetting yoyo on infinite loops. repeat: -1 without yoyo creates a visible snap back to the start after each cycle. Add yoyo: true for continuous, seamless loops.


Related: Duration & Delay · Easing · Stagger