
Learn practical techniques for reducing the flash of unstyled content (FOUC) as your web components/custom elements are defined to improve your users' experience
title: Reducing FOUC with Web Components published: true description: Learn practical techniques for reducing the flash of unstyled content (FOUC) as your web components/custom elements are defined to improve your users' experience tags: css, javascript, webcomponents, customelements cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cvd0gntbnjfkf1ezk6if.jpeg
One of the best things about web components is that they are wicked fast, but one of the common complaints I hear is that, because they are a client-side technology, there can be a delay between when the page renders and the components are defined and rendered. This can result in a flash of unstyled content (FOUC) and page content shifts - that jarring moment when your carefully crafted layout suddenly jumps around before your users' eyes. Not exactly the first impression we're going for.
You can see each of these solutions described in this article in action in this demo site.
Here's the solution that's been making the rounds for ages:
:not(:defined) {
visibility: hidden;
}
This CSS selector targets any custom element that hasn't been defined yet and visually hides it from view. Once the JavaScript defines the custom element, the browser marks it as :defined, the selector no longer matches, and the component becomes visible.
HTMLUnknownElement, which has no intrinsic size. As components load and apply their styles, it can result in content jumps as the page reflows.visibility: hidden; can remove the content from the accessibility tree, which means screen readers and other assistive technologies can't access it. Your content is invisible to both sighted users and accessibility tools, which can affect initial focus.In a previous article I wrote, I show how to use native JavaScript APIs to improve upon the original solution:
<style>
/* Visually block initial render as components get defined */
body:not(.wc-loaded) {
opacity: 0;
}
</style>
<script type="module">
(() => {
// Select all undefined custom elements on the page and wait for them to be loaded.
// Once they are all loaded, add the `wc-loaded` class to the body to make it visible again.
Promise.allSettled(
[...document.querySelectorAll(":not(:defined)")]
.map((component) => {
return customElements.whenDefined(component.localName);
})
).then(() => document.body.classList.add("wc-loaded"));
// Add fallback to add the `wc-loaded` class to the body to make it visible after 200ms.
// This prevents the user from being blocked from using your application in case a component fails to load
// or other undefined elements are being used on the page.
setTimeout(() => document.body.classList.add("wc-loaded"), 200);
})();
</script>
This approach waits for all custom elements to be defined before revealing the page. Here's how it works:
Hide the body: We set opacity: 0 on the body until it gets the wc-loaded class. Unlike visibility: hidden, this keeps content in the accessibility tree - screen readers can still access it.
Query for undefined elements: document.querySelectorAll(":not(:defined)") finds all custom elements that haven't been defined yet.
Wait for definition: For each undefined element, we use customElements.whenDefined(), which returns a Promise that resolves when that custom element gets defined.
Wait for all components: Promise.allSettled() waits for all those Promises to complete (whether they succeed or fail), then adds the wc-loaded class to make everything visible.
Fallback timeout: The setTimeout ensures that even if something goes wrong, users aren't staring at a blank page forever. After 200ms, we show the page regardless.
opacity instead of visibility: hidden, the page content remains available to assistive technologies. Users with screen readers aren't left in the dark.What if we could have the best of both worlds - a solution that's easy to set up, lightweight, doesn't require additional JavaScript to run, and provides a fallback if components fail to load? Here's a CSS-only approach:
/* must define property to work properly for all browsers */
@property --wc-loaded {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
body {
/* start timeout regardless if there are undefined components */
animation: showBody 0s linear 200ms forwards;
/* if there are undefined components, hide until defined or 200ms - whichever comes first */
&:has(:not(:defined)) {
opacity: var(--wc-loaded, 0);
}
}
/* update variable to prevent opactity flash for subsequent component loads */
@keyframes showBody {
to {
--wc-loaded: 1;
}
}
@property --wc-loaded - Defines a custom CSS property with an initial value of 0. This is required for proper browser compatibility and serves as a flag to track component loading state.
animation: showBody 0s linear 200ms forwards - Sets up a zero-duration animation with a 200ms delay. This serves two purposes:
--wc-loaded property to 1 after the delay:not(:defined) - A powerful CSS pseudo-class that selects any custom elements that haven't been registered with the browser yet. This includes all web components that are still loading or haven't been defined.
body:has(:not(:defined)) - Uses the :has() pseudo-class to check if the body contains ANY undefined custom elements. This selector dynamically evaluates as components are defined.
opacity: var(--wc-loaded, 0) - Sets the body opacity to the value of --wc-loaded (defaulting to 0). When undefined components exist:
0 (page hidden)--wc-loaded becomes 1 (page shows):has(:not(:defined)) selector stops matching, removing the opacity override entirely@keyframes showBody - Defines the animation that updates the --wc-loaded property to 1. This prevents opacity flashing when new components are dynamically added to the page after initial load.
The Magic: Once all custom elements are registered, the :not(:defined) selector no longer matches anything, causing the body:has(:not(:defined)) condition to become false. This removes the opacity rule, and the page becomes fully visible. For fast connections, the 200ms timeout ensures minimal delay, while slow connections still show content rather than leaving users with a blank screen indefinitely.
setTimeout calls.:has() selector automatically updates when elements become defined.opacity rather than visibility, keeping content available to assistive technologies.:has() selector is relatively new. It's supported in all modern browsers, but may not be supported in older versions. The good news is that if something is not supported, it will gracefully fail rather than crash your app.Please keep in mind that this is blocking the visibility of the page until the components are defined or until the timeout has lapsed. The longer the delay is, the more it can negatively impact the user experience.
Here are some observations about the length of time:
I recommend keeping the timeout less than 500 milliseconds for the smoothest experience. That will allow your components and page content to load in a uniform manner without perceptible delays for the user.
If your components are taking longer than that to load, you may want to look into a combination of these solutions to provide a good user experience without negatively impacting the overall user experience for the rest of the application.
These solutions will continue to evolve, but FOUC doesn't have to be a necessary evil of using web components. With these techniques in your toolkit, you can provide smooth, accessible experiences for your users and boost those Core Web Vitals scores without sacrificing the benefits of web components.
gemmaI ported the whole Gemma-4 family — E2B, E4B, 12B, 31B, and the 26B-A4B MoE — to run on...
communityHey DEV, I'm Tobore. Let's actually connect. I've been on here for a while now, mostly writing and...
ai(yep, kinda clickbait, just for the funsies 😊) At the beginning of the year, I relaunched my...
aiMy laptop was sitting idle with the fan at full tilt. Nothing was running that I knew of. The culprit...
githubactionsI Built a Thing! TL;DR — Google Gemini-based Pull Request reviews and Issue Triaging for...
aiI've been hearing the word "harness" thrown around a lot lately. I assumed it just meant "the IDE" or...
Workflows from the Neura Market marketplace related to this DeepSeek resource