anime icon indicating copy to clipboard operation
anime copied to clipboard

Feature Proposal: matchMedia-like Responsive Timeline

Open smallma opened this issue 8 months ago • 4 comments

Describe what the feature does Allow developers to define and manage different timelines based on media queries, automatically cleaning up and reinitializing timelines when the media condition changes—similar to GSAP’s matchMedia. This helps support true responsive animation logic across screen sizes, not just value tweaks.

Provide a code example of what the feature should look like

anime.matchMedia({
  '(max-width: 768px)': () => {
    return anime.timeline().add({ targets: '.box', translateX: 100 });
  },
  '(min-width: 769px)': () => {
    return anime.timeline().add({ targets: '.box', translateX: 300 });
  }
});

Describe alternatives you've considered

  • Manually listening to window.matchMedia().addEventListener('change', ...) and writing custom logic to destroy and rebuild timelines.
  • Using createScope() but still needing to manage timeline cleanup and media conditions manually.

Additional context GSAP provides this natively with matchMedia(). It significantly reduces complexity when managing responsive timelines. Anime.js’s createScope() helps isolate logic but doesn’t bind to media query states automatically.

smallma avatar Apr 16 '25 00:04 smallma

Using createScope() but still needing to manage timeline cleanup and media conditions manually.

Can you share an example where you had to cleanup the timeline manually with createScope ? Because this what createScope does:

createScope({
  mediaQueries: {
    maxS: '(max-width: 768px)',
    minM: '(min-width: 769px)'
  }
})
.add(({ matches }) => {
  const tl = createTimeline(); // This timeline will automatically be reverted when one of the media query matches
  if (matches.maxS) tl.add('.square', { x: 100 })
  if (matches.minM) tl.add('.square', { x: 300 })  
});

You can also write this like this:

createScope({
  mediaQueries: {
    maxS: '(max-width: 768px)',
    minM: '(min-width: 769px)'
  }
})
.add(({ matches }) => {
  if (matches.maxS) createTimeline().add('.square', { x: 100 })
  if (matches.minM) createTimeline().add('.square', { x: 300 })  
});

Or like this:

createScope({
  mediaQueries: { maxS: '(max-width: 768px)' }
})
.add(({ matches }) => {
  createTimeline().add('.square', { x: matches.maxS ? 100 : 300 })  
});

All these examples produce the same effects, and the timelines created inside the scopes should all be cleanup properly when a media query change

juliangarnier avatar Apr 16 '25 08:04 juliangarnier

Thanks for your explanation! I understand that createScope helps with automatic cleanup when media queries change — that’s great. But from a practical use case perspective, anime.js still can't easily handle different timelines per RWD breakpoint the way GSAP does with ScrollTrigger.matchMedia().

For example, in GSAP I can write separate timelines for desktop, tablet, and mobile — each with different labels, triggers, start/end positions, durations, delays, even callback logic — all inside one matchMedia() block, and they’re managed seamlessly.

In anime.js, even with createScope, I'd still have to manually recreate those differences in logic, conditions, and animation sequences across breakpoints, which becomes verbose and hard to manage when the timelines are complex.

That’s why a matchMedia-like responsive timeline would be super helpful — especially when building scroll-driven storytelling or complex RWD animations.

ScrollTrigger.matchMedia({
  '(min-width: 1279.5px)': function () {
    const tl = gsap.timeline({
      scrollTrigger: {
        trigger: $target,
        endTrigger: $endTarget,
        start: 'top center',
        end: 'bottom bottom',
        scrub: 1
      }
    });

    const desktopOffset = _getImageOffset();
    tl.set($img, { ...desktopOffset });

    tl.addLabel('step1')
      .to($star, { rotate: -20, duration: 1 }, 'step1')
      .to($star, { x: -70, rotate: -90, scale: 0, duration: 1.5, delay: 1 }, 'step1')
      .to($svgPath, { strokeDashoffset: 0, duration: 1.5, delay: 1 }, 'step1');

    tl.addLabel('step2')
      .to($svg, { scale: 25, rotate: -90, opacity: 0, duration: 3 }, 'step2')
      .to($contentTop, { y: 0, opacity: 1, duration: 1, delay: 0.8 }, 'step2')
      .to($img, {
        rotate: 0, duration: 1.7, delay: 2, onComplete: () => {
          _handleScene1ContentItemActive(-1, 0);
        }
      }, 'step2');
  },

  '(max-width: 1279.4px)': function () {
    const tl = gsap.timeline({
      scrollTrigger: {
        trigger: $target,
        start: 'top center',
        end: 'top top-=20%',
        scrub: 1
      }
    });

    tl.addLabel('step1')
      .to($star, { rotate: -20, duration: 1 }, 'step1')
      .to($star, { x: -70, rotate: -90, scale: 0, duration: 1.5, delay: 1 }, 'step1')
      .to($svgPath, { strokeDashoffset: 0, duration: 1.5, delay: 1 }, 'step1');

    tl.addLabel('step2')
      .to($svg, { scale: 25, rotate: -90, opacity: 0, duration: 3 }, 'step2');
  }
});

smallma avatar Apr 16 '25 08:04 smallma

For example, in GSAP I can write separate timelines for desktop, tablet, and mobile — each with different labels, triggers, start/end positions, durations, delays, even callback logic — all inside one matchMedia() block, and they’re managed seamlessly.

In anime.js, even with createScope, I'd still have to manually recreate those differences in logic, conditions, and animation sequences across breakpoints, which becomes verbose and hard to manage when the timelines are complex.

I don't understand, If you want to have two completely separate timelines between desktop and mobile, you simply create two timelines. The only differences here is that you have to register the mediqueries in the createScope parameters.

createScope({
  mediaQueries: {
    mobile: '(max-width: 768px)',
    desktop: '(min-width: 769px)'
  }
})
.add(({ matches }) => {
  if (matches.mobile) {
    // Mobile timeline code
    const mobileTimeline = createTimeline().add('.square', { x: 100 });
  }
  if (matches.desktop) {
    // Desktop timeline code
    const desktopTimeline = createTimeline().add('.square', { x: 300 });
  }
});

juliangarnier avatar Apr 16 '25 09:04 juliangarnier

You’re right — that approach is definitely a valid direction and makes sense. I’ll go ahead and implement it based on your suggestion for now and will report back with any feedback on its pros and cons after some testing.

Regarding the media queries and timeline separation, thanks for the clarification! That makes it much clearer — defining separate timelines within createScope() using matches for mobile and desktop works well for what I need.

smallma avatar Apr 16 '25 09:04 smallma

I'm closing for now.

Feel free to re-open if needed.

juliangarnier avatar Apr 16 '25 14:04 juliangarnier