Search code examples
javascripthtmlasynchronousbrowsersafari

Updating a <progress> element works in Chrome but not in Safari


I'm trying to update a progress bar from within an async function. The following snippet of code works for me in Chrome but not in Safari:

<!DOCTYPE html>
<html>

<body>
  <progress id="progressBar" value="40" max="100"></progress>
  <script>
    (async () => {
      const progressBar = document.getElementById("progressBar");
      for (let i = 0; i <= 100; i++) {
        progressBar.value = i;
        await new Promise(resolve => setTimeout(resolve, 100)); // sleep for 0.1s
      }
    })();
  </script>
</body>

</html>

In chrome, the progress bar gets updated every 0.1s as expected.

In Safari, the progress bar doesn't get updated (the loop executes, and we can even see that the value of progressBar is being updated by printing console.log(progressBar.value), but that change doesn't get reflected in the UI).

I'm using an M1 Macbook Pro with Safari Version 16.4 (18615.1.26.11.23).


Solution

  • I can reproduce the same bug on the current stable Version 16.4 (18615.1.26.110.1) but not on the latest Technology Preview (Release 170 (Safari 16.4, WebKit 18616.1.14.5)). So I guess we can assume that they already did fix it and that all we have to do is to wait for that fix to be shipped in stable.

    If you need a workaround, one that seems to work is to force the reinsertion of the element in the DOM:

    (async () => {
      const progressBar = document.getElementById("progressBar");
      for (let i = 0; i <= 100; i++) {
        progressBar.value = i;
        // workaround Safari bug not updating the UI
        progressBar.replaceWith(progressBar);
        await new Promise(resolve => setTimeout(resolve, 100)); // sleep for 0.1s
      }
    })();
    <progress id="progressBar" value="40" max="100"></progress>

    Since setting the .value IDL attribute of the <progress> element will also set its value="" content attribute, this shouldn't be as bad as it sounds, because setting the IDL attribute will anyway already trigger a DOM mutation record for the content attribute, so we're just pilling one up on that, in the same mutation event.