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.
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.
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 |
// 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.
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.
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.
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
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.
// 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).
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:
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:
Motion("spinner", ".loader-icon", {
to: { rotate: 360 },
duration: 0.9,
ease: "none",
repeat: -1,
}).onPageLoad(); Looping marquee position — translate then instantly reset:
Motion("marquee", ".track", {
to: { x: "-50%" },
duration: 8,
ease: "none",
repeat: -1,
}).onPageLoad(); Shake on error — finite yoyo for form validation feedback:
Motion("field-shake", ".input-error", {
to: { x: 6 },
duration: 0.07,
ease: "power1.inOut",
repeat: { times: 7, yoyo: true },
}).play(); API Reference
// 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