Flash of Undefined Custom Elements (FOUCE)

Web components are defined and registered with JavaScript. Depending on how and when you load the scripts that perform registration, you may see a brief flash of unstyled HTML where your custom elements should be when the page loads. This is not dissimilar to FOUC, which occurs when HTML is displayed before the stylesheet has loaded.

For reference, here's an exaggerated example of three custom elements loading at different intervals.

Since the world needs more acronyms, and since one doesn't seem to exist yet, I'm calling this phenomenon FOUCE (rhymes with "spouse"), which stands for Flash of Undefined Custom Elements.

Fortunately, the browser gives us some tools to mitigate it.

The :defined selector

One option is to use the :defined CSS pseudo-class to "hide" custom elements that haven't been registered yet. You can scope it to specific tags or you can hide all undefined custom elements as shown below.

:not(:defined) {
  visibility: hidden;
}

As soon as a custom element is registered, it will immediately appear with all of its styles, effectively eliminating FOUCE. Note the use of visibility: hidden instead of display: none to reduce shifting as elements are registered.

The drawback to this approach is that custom elements can potentially appear one by one instead of all at the same time.

That's certainly a lot better, but can we take things a bit further?

Awaiting customElements.whenDefined()

Another option is to use customElements.whenDefined(), which returns a promise that resolves when the specified element gets registered. You'll probably want to use it with Promise.allSettled() in case an element fails to load for some reason (thanks, Westbrook!).

A clever way to use this method is to hide the <body> with opacity: 0 and add a class that fades it in as soon as all your custom elements are defined.

<style>
  body {
    opacity: 0;
  }

  body.ready {
    opacity: 1;
    transition: .25s opacity;
  }
</style>

<script type="module">
  await Promise.allSettled([
    customElements.whenDefined('my-button'),
    customElements.whenDefined('my-card'),
    customElements.whenDefined('my-rating')
  ]);

  // Button, card, and rating are registered now! Add
  // the `ready` class so the UI fades in.
  document.body.classList.add('ready');
</script>

In my opinion, this is the better approach because it subtly fades in the entire page as soon as all your custom elements are registered. After all, what's the point of showing the page before it's ready?

The drawback, of course, is that you need to keep track of which elements you're using and add them to the list. But this can also be an advantage if your initial UI only requires a handful of custom elements. For example, you can load only the ones you need upfront and let the rest of them load asynchronously to make your page load faster.

Have you used either of these methods to prevent FOUCE? Have you thought of a better way? Let me know on Twitter!


December 30, 2021: The original version of this article mentioned script placement in the <head> as a method of eliminating FOUCE, but that doesn't work if you're using ES modules. While the approach works for non-modules, I've removed it because it leads to poor page load times and because of the growing ubiquity of ES modules on the Web.

This article was originally published on the author's blog.

21