3 Methods for Scoped Styles in Web Components That Work Everywhere
Web components are great. They’re versatile. They can be rendered on the server or (very much less preferably) on the client. They may or may not need interactivity or client-side JavaScript. And unlike heftier legacy frameworks like React, when you need to add interactivity they can still be extremely lightweight by leveraging (dare I say it…) The Platform™ *audible gasp*.
But let’s not get distracted from the task at hand: we want to evaluate a few different options on the table to apply styles (CSS) to our web components.
Here are the things we want to think about:
- Encapsulation. We don’t want styles from distinct components to interfere with each other (that’s what this blog post is here for, after all).
- Performance. Predictably, a method popularized for CSS encapsulation (CSS-in-JS) was less than ideal because it was slow-by-default. Let’s keep performance at the forefront of our minds.
- Browser compatibility. Let’s not go so far out on the cutting edge that we leave some of our visitors behind.
- Code re-use without duplication, during both authoring and the output. We want to streamline our output to avoid sending any more code down the wire than we need to.
- Client-side framework independent. The methods described in this post do not use any client-side libraries or frameworks.
Methods for encapsulated styles:
- Declarative Shadow DOM
- Shadow DOM
- WebC
- Future bonus method: CSS
@scope
1. Declarative Shadow DOM
This is the newest kid on the block. And with Safari recently shipping support (in 16.4), only Firefox is now conspicuously missing from the evergreens.
Declarative Shadow DOM is ⚠️ not supported in your browser and will require the polyfill.
Declarative Shadow DOM ✅ is supported in your browser.
Note that the underline is restricted to the component and the component only as Declarative Shadow DOM styles are encapsulated for free by the platform! Awesome!
It’s also worth noting that Shadow DOM is not completely isolated from its host page—some styles are inherited! Chris Haynes elaborates in this 2019 post: Why is my Web Component inheriting styles?
Expand to learn about shadowroot
versus shadowrootmode
Astute observers may note that the Can I Use support table is for the shadowroot
attribute, the non-streaming version of Declarative Shadow DOM:
shadowrootmode
Streaming-friendlyshadowroot
Not streaming friendly (deprecated)
You can use them both together I suppose but rolling with shadowrootmode
only is probably your best bet moving forward.
You can view the browser support for shadowrootmode
specifically but it is currently inaccurate. I filed a PR to fix it!
The big drawback of Declarative Shadow DOM comes when you have multiple instances of the same component on the page: you need to duplicate the styles and template content in each instance (not ideal).
Expand to see an example.
It’s worth noting that WebC can assist you when you’re authoring components with Declarative Shadow DOM so that you don’t have to duplicate this template content yourself!
Polyfill and Client-side JavaScript
If you want to add additional clientside interactivity to the component, use the Custom Elements API to do so. This is recommended and required for Declarative Shadow DOM components in Firefox, currently lacking support and requiring a polyfill.
This is less than ideal, as it places a JavaScript dependency on CSS (in Firefox only).
Expand to see the Declarative Shadow DOM polyfill code.
Summary
- Performance ★★★☆
- ✅ Server rendered content.
- ✅ No JavaScript requirement to apply encapsulated styles (except Firefox, keep reading).
- Compatibility ★★☆☆
- This is a very new feature that just shipped in Safari.
- ❌ (Temporary) Firefox requires a JavaScript polyfill, which means that Firefox does have a JS dependency on CSS.
- Duplication ★☆☆☆
- ❌ Declarative Shadow DOM template markup is duplicated throughout every component instance. Whew! This is less than ideal but it’s important to remember that it’s still much faster than alternative CSS-in-JS methods.
2. Shadow DOM
This method uses JavaScript to client-render markup into Shadow DOM. Here’s a code sample of how two instances of <sample-component>
might look in your editor:
Again the benefit here is that the styles applied do not leak out—they’re encapsulated by Shadow DOM—but we did need to use JavaScript to attach the styles.
Summary
Client-rendering is limiting here. While I wouldn’t be so prescriptive to say that it isn’t useful (some features are secondary/optional after all, depending on your use case and requirements)—but it does heavily limit where the approach can be applied.
- Performance ★☆☆☆
- ❌ Client rendered content.
- ❌ JavaScript requirement to apply encapsulated styles.
- Compatibility ★★★☆
- ✅❌ Very broad browser support but I gotta dock one star for JavaScript-generated content, which (even independent of performance) can have further implications for SEO et al.
- Duplication ★★★★
- ✅ Only one instance of the template code is required on a page and can be re-used by every component instance.
3. WebC
Haven’t heard of WebC? It’s a single file component format for Web Components—and it works great with Eleventy. Learn more on the WebC docs.
Consider a WebC component file sample-component.webc
with the following content:
And on our page we’ll use it twice to show how it scales:
This is how the above template renders:
As you can see from the rendered code, the webc:scoped
feature generates a component specific class name and adds that to the component for encapsulated styles. The class is shared across instances and the component CSS is only added once per page.
This allows you to author your component CSS without additional ceremony and WebC will compile it to CSS that has an extremely wide browser compatibility profile (working in Firefox without JS and even in legacy versions of the evergreens).
Client JavaScript
WebC de-duplicates JS in the same way as CSS too. This means we can add <script>
in the component file for our Custom Element client JavaScript and this code will only appear on the page once, no matter how many instances of the component exist on the page.
Expand to see sample-component.webc
using the Custom Elements API
Summary
I won’t give star ratings to something I built 😅 but I do think WebC allows folks to broaden access to things built with web components without the drawbacks of other methods!
- Performance
- ✅ Server rendered content.
- ✅ No JavaScript requirement to apply encapsulated styles.
- Compatibility
- ✅ The broadest browser support.
- Duplication
- ✅ The component code lives in the component file, editable in one place. The CSS only appears once on a page, independent of how many times you use the component.
It’s also worth noting here that Declarative Shadow DOM and Shadow DOM methods can be used with WebC too! Have a look at sample WebC components for each of the methods documented here on @11ty/demo-webc-shadow-dom
.
4. CSS @scope
While not currently available, platform-support for scoping without Shadow DOM may be coming and Miriam Suzanne is leading the charge!
Learn more:
- https://css.oddbird.net/scope/explainer/
- https://github.com/w3c/csswg-drafts/issues/5809
- https://drafts.csswg.org/css-cascade-6/#scoped-styles
Keep an eye on this method—it’s exciting!
7 Comments
Stuart Langridge
@zachleat I was thinking, what the hell is this webc thing? Browsers do this now? And then I realised what it is :) Maybe the pros and cons section should have “requires a build step” item and maybe a “requires 11ty” item? But that aside, this is neat and a really useful summary,… Truncated
Zach Leatherman :11ty:
@sil Good feedback, thank you!
Mayank
@zachleat one thing i'm looking forward to in `@scope` is the concept of a "lower boundary" which prevents styles from leaking into children
Zach Leatherman :11ty:
@hi_mayank *yes* that will be excellent! cc @mia
westbrook
@zachleat I’d love to know more about the actual time difference to go from three starts to one in the performance section for DSD to CSSD. I get the star cost of requiring JS, but for such a trivial component, I’d say the difference would be negligible and for more complex compo… Truncated
westbrook
@zachleat It would also be cool to see if the duplication costs you‘ve listed are dev time or view time expenses. With a tool, like 11ty, you’re not quoted to duplicate that data yourself and with gzip/brotli the actual file size costs of that duplication seem to be mostly nullif… Truncated
westbrook
@hi_mayank @zachleat is this not what the next level of shadow DOM is for!?! 🤪