Repeat & Yoyo

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

See it live Loops feature page

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.

Property Type Default Description
times number Additional repetitions. -1 = infinite
yoyo boolean false Reverse direction on every other cycle
delay number 0 Seconds 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