Chrome does some weird stuff if you toggle a stylesheet on and off
Tags: web, css, software, google
Published: 2021-10-21 11:53:34

---
title: Chrome does some weird stuff if you toggle a stylesheet on and off
published: true
date: 2021-10-21 11:52:00 UTC
tags: #web #css #software #google 
canonical_url: https://mmazzarolo.com/blog/2021-10-10-on-toggling-stylesheets/
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x7y0leudjj2yd26v9c8u.png
---

Last week I spent more time than I expected debugging a Chrome-specific issue: when toggling the visibility of a view of one of our web apps, the view was flashing with unstyled content before becoming visible.

The flow that was causing the issue is the following:

1. At some point, during the user session, we hide a view and disable the stylesheet (`<link href="stylesheet">`) associated with it (it doesn’t really matter **why** we’re disabling it).
2. Later on, when needed, we re-enable the stylesheet.
3. Immediately after re-enabling the stylesheet, we show the view (e.g., by changing its visibility from `display: none` to `display: block`).

To toggle the stylesheet on and off, we are using the `disabled` property of the [StyleSheet interface](https://developer.mozilla.org/en-US/docs/Web/API/StyleSheet).

At this stage, the stylesheet had already been loaded once (before step 1), so expected that re-enabling it (in step 2) would have applied its styles immediately.

Unfortunately, that’s not what happens in Chrome.  
When you re-enable a stylesheet, Chrome (v94.0.4606.71) **sometimes** tries to fetch it:

![devtools](https://mmazzarolo.com/static/7013393d25c164102b7832d3d20cc990/1e043/devtools.png "devtools")

The fetch results in loading the stylesheet from the cache. Still, the browser runs this flow asynchronously, causing a quick flash on unstyled content until the stylesheet is fully loaded:

![](https://mmazzarolo.com/34ce67587fb6a0915b21b45523236747/example-flash.gif)

You can reproduce this issue with the following HTML code:

```html
<html>
  <body>
    <link
      rel="stylesheet"
      href="https://unpkg.com/purecss@2.0.6/build/pure-min.css"
      id="purecss"
    />
    <button id="btn">Toggle CSS</button>
    <h1 id="text">Hello world</h1>
  </body>
  <script>
    const purecss = document.querySelector("#purecss");
    const btn = document.querySelector("#btn");
    const text = document.querySelector("#text");
    btn.addEventListener("click", () => {
      if (purecss.disabled) {
        purecss.disabled = false;
        // At this point, the stylesheet on the page should reflect the state
        // set in the previous line — but in Chrome, sometimes it wont.
        // To reproduce the issue consistently, tick "Disable cache" in
        // the network panel.
        text.style.display = "block";
        debugger;
      } else {
        text.style.display = "none";
        purecss.disabled = true;
      }
    });
  </script>
</html>
```

If you run your code on Chrome, sometimes you’ll notice that the breakpoint will stop in a state where the stylesheet is not fully loaded.  
For example, in the screenshot below, Firefox and Chrome are paused on the same breakpoint. As you can see, in Chrome there’s a pending network request for the stylesheet, and the style has not been applied yet (see the text font style).

![firefox vs chrome](https://mmazzarolo.com/static/5a40d763eeaf7a433f25dfd46b3f044b/1e043/firefox-vs-chrome.png "firefox vs chrome")

I don’t know if this issue is a Chrome bug or not, but I have an idea on why it might be happening.  
When you add a `disabled` stylesheet to the page (with `<link href="stylesheet" disabled>`) and then enable it a runtime dynamically, browsers load the stylesheet on-demand.  
My guess is that sometimes Chrome tries to load the stylesheet on-demand even if it has already been loaded before.

To solve this issue, we must wait for the stylesheet to be fully loaded before showing the view.  
Unfortunately, toggling a stylesheet on and off doesn’t trigger its `onload` event.  
As a (less elegant) alternative, we can use the [`document.styleSheets`](https://developer.mozilla.org/en-US/docs/Web/API/Document/styleSheets) API. This API is a read-only property that returns the list of stylesheets explicitly linked into or embedded in the document. In Chrome, we can expect to find our re-enabled stylesheet in this list only after it has been fully loaded.

So, we can update our flow this way:

1. At some point, during the user session, we hide a view and disable the stylesheet (`<link href="stylesheet">`) associated with it (it doesn’t really matter **why** we’re disabling it).
2. Later on, when needed, we re-enable the stylesheet.
3. Wait for the stylesheet to be available in `document.styleSheet` (using a quick-looped `setInterval`).
4. Immediately after re-enabling the stylesheet, we show the view (e.g., by changing its visibility from `display: none` to `display: block`).

Example of the solution:

```html
<html>
  <body>
    <link
      rel="stylesheet"
      href="https://unpkg.com/purecss@2.0.6/build/pure-min.css"
      id="purecss"
    />
    <button id="btn">Toggle CSS</button>
    <h1 id="text">Hello world</h1>
  </body>
  <script>
    const purecss = document.querySelector("#purecss");
    const btn = document.querySelector("#btn");
    const text = document.querySelector("#text");

    function isStylesheetLoaded() {
      return [...document.styleSheets].find(
        (stylesheet) => stylesheet.href === purecss.href
      );
    }

    // Wait for the stylesheet to be loaded
    async function waitForStylesheet() {
      if (isStylesheetLoaded()) {
        // In non-Chromium browsers the stylesheet will immediately be loaded
        return;
      }
      const intervalMs = 20;
      return new Promise((resolve) => {
        const intervalId = setInterval(() => {
          if (isStylesheetLoaded()) {
            clearInterval(intervalId);
            return resolve();
          }
          // Handle max retries/timeout if needed.
        });
      });
    }

    btn.addEventListener("click", async () => {
      if (purecss.disabled) {
        purecss.disabled = false;
        await waitForStylesheet(); // 👈👈👈
        text.style.display = "block";
        debugger;
      } else {
        text.style.display = "none";
        purecss.disabled = true;
      }
    });
  </script>
</html>
```