1

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>
Oliver
  • 2,930
  • 1
  • 20
  • 24

1 Answers1

2

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

Oliver
  • 2,930
  • 1
  • 20
  • 24
  • I encountered a similar issue, the way I solved it was by changing the keyframe name through js by using CSSOM (CSS Object Model), then update the animationName on the element. Yours is a much cleaner solution. This leads me to believe that in Safari, when an animation is applied to an element, it is cached, but doesn't listen for any update, even through CSSOM. – Caleb Taylor Jan 04 '21 at 02:17