3

I'm developing a lightbox on my website and noticed that it flickers on iOS in all browsers I had (Brave, Chrome, and Safari). However, it doesn't flicker always, just when I move the image way to the left, so that it's out of the viewport.

There are dozens of answers out there suggesting properties such as:

-webkit-backface-visibility: hidden;
-webkit-transform-style: preserve-3d;
-webkit-transform: translate3d(0, 0, 0);
-webkit-perspective: 1000;

I've tried all of those, but none of them worked. This was kind of expected, since those questions are pretty old and I thought such issues were long gone. It appears that's not the case, however.

After testing out various combinations of CSS properties and DOM changes, I boiled my code down until I was left with a minimal reproducible example:

let box = document.querySelector(".lightbox");
let img = box.querySelector("img");
let rx = 0;
let ry = 0;
let x = 0;
let y = 0;

let render = () => {
  img.style.transform = `translate(${rx}px, ${ry}px) scale(2)`;
};

let onDown = (e) => {
  x = e.clientX;
  y = e.clientY;
  box.addEventListener("pointermove", onMove);
};

let onMove = (e) => {
  let lastX = x;
  let lastY = y;
  x = e.clientX;
  y = e.clientY;
  if (lastX && lastY) {
    rx += x - lastX;
    ry += y - lastY;
  }
  render();
};

let onUp = (e) => {
  box.removeEventListener("pointermove", onMove);
};

box.addEventListener("pointerdown", onDown);
box.addEventListener("pointerup", onUp);
box.addEventListener("pointerleave", onUp);
box.addEventListener("pointercancel", onUp);

render();

setInterval(() => {
  box.classList.remove("test");
  setTimeout(() => {
    box.classList.add("test");
  }, 100);
}, 1e3);
img {
  max-width: 100%;
  height: auto;
}

.lightbox {
  width: 100%;
  height: 100%;
  touch-action: none;
}

.lightbox img {
  transition: transform 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}

.lightbox.test img {
  transition: none;
}
<div class="lightbox">
  <img width="1280" height="720" style="background-image: linear-gradient(-45deg, #cc8888, #88cc88)" />
</div>

If you're wondering why I have a setInterval that is adding and removing a class, it's because it triggers the bug. If there's no interval and the class is simply added/removed after the pointerup event, for example, it doesn't flicker. The scale(2) in the transform is also part of the issue. If I remove it, the flickers stop.

I've made a demo site on Netlify and a demo video.

Edit: I've managed to simplify it further:

let box = document.querySelector("div");
let rx = 0;
let ry = 0;
let x = 0;
let y = 0;

let render = () => {
  box.style.transform = `translate(${rx}px, ${ry}px) scale(2)`;
};

let onDown = (e) => {
  x = e.clientX;
  y = e.clientY;
  box.addEventListener("pointermove", onMove);
};

let onMove = (e) => {
  let lastX = x;
  let lastY = y;
  x = e.clientX;
  y = e.clientY;
  if (lastX && lastY) {
    rx += x - lastX;
    ry += y - lastY;
  }
  render();
};

let onUp = (e) => {
  box.removeEventListener("pointermove", onMove);
};

box.addEventListener("pointerdown", onDown);
box.addEventListener("pointerup", onUp);
box.addEventListener("pointerleave", onUp);
box.addEventListener("pointercancel", onUp);

render();
div {
  width: 200px;
  height: 200px;
  touch-action: none;
  transition: transform 0.1s ease;
  background-image: linear-gradient(-45deg, #cc8888, #88cc88);
}
<div></div>

In this snippet, there's no setInterval and class toggling. Instead, the transition is always left active. Now, the element is hidden as long as you're moving the image and it's on the left side of the screen. It appears that the issue only goes away if I remove the scale() part of the transform, i.e. change it from this:

box.style.transform = `translate(${rx}px, ${ry}px) scale(2)`;

...to this:

box.style.transform = `translate(${rx}px, ${ry}px)`;

However, in my original application, this scale changes dynamically, so I need it.

dodov
  • 5,206
  • 3
  • 34
  • 65

1 Answers1

1

Using transform matrix() appears to solve the issue. If I switch this line:

box.style.transform = `translate(${rx}px, ${ry}px) scale(2)`;

with this:

box.style.transform = `matrix(2, 0, 0, 2, ${rx}, ${ry})`;

the issue goes away. Tested on iOS Brave, Chrome, and Safari.

dodov
  • 5,206
  • 3
  • 34
  • 65