Skip to content

Fix repeat options in AnimationSequence#3650

Open
mattgperry wants to merge 1 commit intomainfrom
worktree-fix-issue-2915
Open

Fix repeat options in AnimationSequence#3650
mattgperry wants to merge 1 commit intomainfrom
worktree-fix-issue-2915

Conversation

@mattgperry
Copy link
Collaborator

Summary

  • Fixes repeat options (including repeat: Infinity) being broken when used in AnimationSequence segment transitions
  • When repeat >= 20 (including Infinity), passes repeat/repeatType/repeatDelay through to the final transition instead of attempting keyframe expansion (which threw in dev or caused an infinite loop in production)
  • Finite repeats < 20 continue to use existing keyframe expansion behavior

Bug

When passing an array of animate definitions to animate(), repeat options in segment transitions were broken for large/infinite values. The sequence builder tried to expand keyframes inline for all repeat counts, but:

  • repeat: Infinity triggered an invariant error in dev ("Repeat count too high")
  • In production (where invariant is a noop), it caused an infinite for loop

Fix

For repeat >= MAX_REPEAT (20), skip keyframe expansion and instead store the repeat options (repeat, repeatType, repeatDelay) to be passed through to the final transition object. The animation engine already handles these properties natively, so no keyframe expansion is needed.

Fixes #2915

🤖 Generated with Claude Code

When repeat >= 20 (including Infinity) was used in a sequence segment
transition, it either threw an error in dev or caused an infinite loop
in production. Instead of trying to expand keyframes for large repeat
counts, pass repeat/repeatType/repeatDelay through to the final
transition and let the animation engine handle repeating natively.

Fixes #2915

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@greptile-apps
Copy link

greptile-apps bot commented Mar 17, 2026

Greptile Summary

This PR fixes a crash (invariant error in dev) and an infinite loop (in prod) when using repeat: Infinity or any repeat >= 20 inside an AnimationSequence segment transition. The fix introduces a repeatPassthrough Map that stores repeat options for high/infinite repeat counts and injects them directly into the final per-value transition, allowing the animation engine to handle repetition natively rather than trying to expand keyframes inline.

Key changes:

  • Removes the invariant guard on repeat < MAX_REPEAT and instead splits the if (repeat) block into two paths: keyframe expansion for repeat < 20, and passthrough for repeat >= 20.
  • Adds a repeatPassthrough Map (keyed by ValueSequence) that accumulates { repeat, repeatType, repeatDelay } for the passthrough case and spreads it into the final transition object.
  • Adds a test confirming repeat: Infinity propagates through to the final transition correctly.

Issues found:

  • For finite repeat >= MAX_REPEAT (e.g. repeat: 25), duration is not updated via calculateRepeatDuration, so subsequent segments in a multi-step sequence will start at the wrong time — positioned after the base single-iteration duration rather than after all repetitions. This creates a hard timing discontinuity at the MAX_REPEAT = 20 boundary.
  • repeatPassthrough unconditionally stores repeatType: undefined when repeatType is not provided, which gets spread into the final transition and can silently override any repeatType set in sequenceTransition.
  • Test coverage doesn't include repeatType/repeatDelay passthrough or multi-segment sequences with finite large repeat counts.

Confidence Score: 3/5

  • Safe for the primary repeat: Infinity use case, but introduces a timing regression for finite repeat >= 20 in multi-segment sequences.
  • The fix correctly solves the crash/infinite-loop for repeat: Infinity and low-repeat paths are unchanged. However, finite repeat values between 20 and Infinity now silently produce incorrect segment timing in multi-step sequences — a behavioral regression from "throws in dev" to "wrong output with no warning". The repeatType: undefined spread issue is a smaller concern but could surprise in edge cases.
  • packages/framer-motion/src/animation/sequence/create.ts — specifically the passthrough branch where duration is not adjusted for finite repeat >= MAX_REPEAT.

Important Files Changed

Filename Overview
packages/framer-motion/src/animation/sequence/create.ts Core sequence builder: adds repeatPassthrough Map and a repeat >= MAX_REPEAT branch to skip keyframe expansion — fixes the crash/infinite-loop, but finite repeats ≥ 20 don't update duration, causing incorrect timing for subsequent segments in multi-step sequences; also unconditionally spreads repeatType: undefined which can silently override sequenceTransition.repeatType.
packages/framer-motion/src/animation/sequence/tests/index.test.ts Adds one test for repeat: Infinity passthrough covering keyframes, duration, times, ease, and repeat fields — happy-path coverage is good, but lacks tests for repeatType/repeatDelay passthrough and for multi-segment sequences with finite large repeat counts.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["resolveValueSequence()"] --> B{repeat defined?}
    B -- No --> G[Proceed to addKeyframes]
    B -- Yes --> C{repeat >= MAX_REPEAT\ni.e. >= 20?}
    C -- Yes --> D["Store in repeatPassthrough Map\n{ repeat, repeatType, repeatDelay }"]
    D --> E["duration stays as base duration\n⚠️ NOT expanded for finite repeat >= 20"]
    E --> G
    C -- No --> F["calculateRepeatDuration()\nExpand keyframes inline\nExpand times & ease arrays\nnormalizeTimes()"]
    F --> G
    G["addKeyframes()\nupdate maxDuration / totalDuration"] --> H["Second pass: build final transition"]
    H --> I["definition.transition[key] = {\n  ...remainingDefaultTransition,\n  duration: totalDuration,\n  ease, times,\n  ...sequenceTransition,\n  ...repeatPassthrough.get(valueSequence) 👈 new\n}"]
Loading

Last reviewed commit: 137e874

Comment on lines 209 to +221
if (repeat) {
invariant(
repeat < MAX_REPEAT,
"Repeat count too high, must be less than 20",
"repeat-count-high"
)

duration = calculateRepeatDuration(
duration,
repeat,
repeatDelay
)

const originalKeyframes = [...valueKeyframesAsList]
const originalTimes = [...times]
ease = Array.isArray(ease) ? [...ease] : [ease]
const originalEase = [...ease]
if (repeat >= MAX_REPEAT) {
/**
* For large/infinite repeat counts, don't expand keyframes.
* Pass repeat options through to the final transition
* and let the animation engine handle repeating.
*/
repeatPassthrough.set(valueSequence, {
repeat,
repeatType: repeatType as Transition["repeatType"],
repeatDelay: repeatDelay || undefined,
})
} else {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duration not adjusted for finite repeat >= MAX_REPEAT in multi-segment sequences

When repeat >= 20 but is still finite (e.g., repeat: 25), the passthrough path skips calling calculateRepeatDuration, so duration remains the base per-iteration duration. This means maxDuration and targetTime — used to position subsequent segments in the sequence — are based only on the single-iteration duration.

Consider this scenario:

animate([
  [el, { x: [0, 100] }, { duration: 1, repeat: 25 }],
  [el, { y: [0, 100] }, { duration: 1 }], // Should start at t=26s, actually starts at t=1s
])

For repeat: 19 (just below threshold), calculateRepeatDuration is called and the next segment correctly starts at 20s. For repeat: 20, it starts at 1s instead of 21s. There's a hard discontinuity at the MAX_REPEAT boundary.

For repeat: Infinity this is unavoidable (infinite duration can't be expanded), but for finite repeat >= 20, the correct duration could still be computed with calculateRepeatDuration and applied to maxDuration/targetTime, even if keyframe expansion is skipped.

if (repeat >= MAX_REPEAT) {
    repeatPassthrough.set(valueSequence, {
        repeat,
        repeatType: repeatType as Transition["repeatType"],
        repeatDelay: repeatDelay || undefined,
    })
    // Still adjust duration for finite repeats so subsequent segments are placed correctly
    if (isFinite(repeat)) {
        duration = calculateRepeatDuration(duration, repeat, repeatDelay)
    }
} else {

Comment on lines +216 to +220
repeatPassthrough.set(valueSequence, {
repeat,
repeatType: repeatType as Transition["repeatType"],
repeatDelay: repeatDelay || undefined,
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 repeatType: undefined spreads into final transition unconditionally

When the user doesn't provide repeatType, it destructures as undefined. Spreading { repeatType: undefined } into the final transition object will set repeatType to undefined as an explicit key, potentially overriding any repeatType set in sequenceTransition (which is spread just before this in line 409).

Suggested change
repeatPassthrough.set(valueSequence, {
repeat,
repeatType: repeatType as Transition["repeatType"],
repeatDelay: repeatDelay || undefined,
})
repeatPassthrough.set(valueSequence, {
repeat,
...(repeatType !== undefined && { repeatType: repeatType as Transition["repeatType"] }),
...(repeatDelay ? { repeatDelay } : {}),
})

Comment on lines +733 to +754
test("It passes repeat: Infinity through to the final transition (#2915)", () => {
const animations = createAnimationsFromSequence(
[
[
a,
{ x: [0, 100] },
{ duration: 1, repeat: Infinity, ease: "linear" },
],
],
undefined,
undefined,
{ spring }
)

expect(animations.get(a)!.keyframes.x).toEqual([0, 100])
const { duration, times, ease, repeat } =
animations.get(a)!.transition.x
expect(duration).toEqual(1)
expect(times).toEqual([0, 1])
expect(ease).toEqual(["linear", "linear"])
expect(repeat).toEqual(Infinity)
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing test coverage for passthrough edge cases

The new test covers repeat: Infinity but leaves several important cases untested:

  1. repeatType passthrough: No assertion that repeatType (e.g. "mirror") is correctly propagated through to the final transition.
  2. repeatDelay passthrough: No assertion that a non-zero repeatDelay makes it to the final transition.
  3. Finite repeat >= MAX_REPEAT in multi-segment sequences: A test like the existing "Repeating a segment correctly places the next segment at the end" test but with repeat: 25 would expose the timing discontinuity noted in the logic comment above.
  4. repeat: 20 boundary: This is the smallest value that now follows the passthrough path, and its behavior (timing for subsequent segments) is not exercised at all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Fix repeat options in AnimationSequence

1 participant