Search code examples
csssafariinlinekeyframeprogrammatically-created

Safari ignoring inline styles for keyframe animations


When setting @keyframes using CSS transform in Safari (desktop and iOS) with just an end frame, and then updating the starting transform position programmatically (inline via JS), Safari appears to continue to execute the animation from the initial position (i.e. using the initial transform value in the stylesheet) - not using the inline style.

In short, Safari keyframe animations don't appear to update to reflect any changes to inline styles that are added programmatically

The below snippet (reduced test case) works in both Firefox and Chrome, but Safari just animates to the position of 0. My question is whether or not there is a workaround for this? Is this an intended representation of the spec by Apple, and should programmatic inline styles be ignored for keyframe animations? I can't imagine that they should.

I have tried cloning and replacing the element, to no avail. I also tested with the inline style being included onLoad, and this did work in Safari, so the bug seems to only be when the style is added programmatically.

I appreciate I can animate entirely using JS and requestAnimationFrame, which is my fallback solution if the above fails. (Likewise, a great solution is to use the Web Animation API, which seems exactly made for this purpose. But support for that is patchy, so a no-go for now). What I am really interested in is a solution to the above bug, rather than alternative suggestions.

(As an aside, the <marquee> tag still works as a cross-browser solution... *shudder*)

// Get slides
let marquee = document.querySelector('.mock-marquee__content');

// Transform slides for initial offset
marquee.style.transform = `translate(-${marquee.clientWidth}px, 0)`;
.mock-marquee {
  overflow: hidden;
  display: flex;
}
.mock-marquee__content {
  white-space: nowrap;
  display: flex;
  animation: marquee 5s linear infinite;
}
.mock-marquee__content__slide {
  display: block;
}
.mock-marquee__content__slide:not(:first-child) {
  margin-left: 100px;
}
.mock-marquee__content:hover {
  animation-play-state: paused;
}

@keyframes marquee {
  from {
    transform: translate(100vw, 0);
  }
}
<div class="mock-marquee">
  <div class="mock-marquee__content">
    <span class="mock-marquee__content__slide">Hello</span>
    <span class="mock-marquee__content__slide">world</span>
    <span class="mock-marquee__content__slide">Foo</span>
    <span class="mock-marquee__content__slide">Bar</span>
    <span class="mock-marquee__content__slide">Bat</span>
    <span class="mock-marquee__content__slide">Another one</span>
    <span class="mock-marquee__content__slide">And another! Wowz</span>
  </div>
</div>


Solution

  • Solved it! Solution was simply to force a queue when removing/adding the node, using a setTimeout. Like so:

    window.addEventListener('load', () => {
      let marqueeWrapper = marquee.parentNode;
    
      // Remove marquee
      marquee.remove();
    
      // Re-add, with forced queuing
      setTimeout(() => {
        marqueeWrapper.append(marquee);
      }, 0)
    })
    

    This forces Safari to completely re-render the node, and appears to fix the issue