Promo! Get Web Components Demystified for just $25. Use code TWENTYFIVE

SJ (Scott Jehl)

Enhancing Web Components Safely with Self-Destructing CSS

photo of scott jehl profile headshot

By Scott Jehl

A month ago, I wrote about a defensive CSS convention I've been calling Self-Destructing CSS. As CSS patterns go these days this one is delightfully low-tech, but in my work I've seen it have an outsized impact in ensuring usable experiences for increasingly-heavy websites when network or device conditions aren't ideal.

This notion of delivering "optimistically" while planning for failure is something I've written about before, but the set-it-and-forget-it nature of this latest stab at it makes it my favorite yet. And funny enough, while it's just another take on careful Progressive Enhancement, this one actually feels more like "graceful degradation" to me! Hmm. I won't dwell on that one.

Anyway, last week, Brian Kardell pointed out to me over on Bluesky that the problem Self-Destructing CSS addresses reminded him of one that commonly occurs with Web Components, when developers hide custom elements until they are "defined" by their JavaScript. That's an anti-pattern I covered in my Web Components course, and it's common because it aims to work around a very real issue: often, we don't want our users to see a component until it's "ready."

That (anti)-pattern looks like this:

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

Let's break that down. CSS gives us the pseudo-state :defined which will match any custom element once it is defined by JavaScript. For example, if you have an element <my-element> in your HTML, it starts out essentially a div with a more descriptive name. That "~div" could be useful on its own, but typically developers will define its custom behavior using the customElements APIs now built into browsers (effectively turning it into a "web component," if you please). That element definition looks like this:

customElements.define('my-element', class extends HTMLElement {
  /*...lifecycle additions can go here */
});

Once defined, my-element will be selectable in CSS via :defined, along with all of the other "defined" HTML elements. To be clear, that includes every built-in element defined by the browser, so that :defined selector alone isn't terribly useful on its own, and might do better paired with an element selector instead. That said, if we're trying to style all undefined custom elements to hide them before they're defined, then a selector that matches the opposite of :defined is pretty handy. That's how folks arrived at hiding content with :not(:defined) { visibility: hidden; }.

Unfortunately, while this pattern is clever, it's also risky: if JS fails to run or load in any reasonable amount of time, those elements will remain hidden. I've seen this pattern cause content to be invisible for long periods while popular budget-tier devices struggle to churn through massive JavaScript bundles, even if they're on a fast network.

After a brief delay, hiding content entirely just isn't worth whatever effect you're hoping to achieve.

Improving :not(:defined) with Self-Destructing CSS

As I commented in this HTML standard issue about it, this situation is a great use case for self-destructing CSS.

So moving forward, if you need to hide un-enhanced Custom Elements, I suggest you do not do this:

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

And instead, do this!

@keyframes hideBriefly {
  0%, 100% { visibility: hidden; } 
}
:not(:defined) { animation: hideBriefly 2s; }

Here, just as before, as soon as your element gets defined, the hiding will instantly and automatically unapply. But it will also unapply itself after two seconds no matter what, should the JavaScript take that long to do its thing, or fail to run at all. That last part is critical, because in our medium, we can't expect things to just work like they do on our machine. In fact, they usually don't.

Anyway, that's Self-Destructing CSS for web components. Go play some defense out there on behalf of your users! Oh, and if you want to take this the extra mile, make sure that your initially-delivered HTML is useful/meaningful/functional before the JavaScript loads. Otherwise, you might as well just hide it without a fallback, I guess!