Zach’s ugly mug (his face) Zach Leatherman

3 Methods for Scoped Styles in Web Components That Work Everywhere

April 06, 2023

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:

  1. 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).
  2. 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.
  3. Browser compatibility. Let’s not go so far out on the cutting edge that we leave some of our visitors behind.
  4. 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.
  5. Client-side framework independent. The methods described in this post do not use any client-side libraries or frameworks.

Methods for encapsulated styles:

  1. Declarative Shadow DOM
  2. Shadow DOM
  3. WebC
  4. 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.

Fallback content.

Declarative Shadow DOM is ⚠️ not supported in your browser and will require the polyfill.

Before. Fallback content. After.

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: IndieWeb Avatar for 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-friendly
  • shadowroot 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. Fallback content. Fallback content.

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.


  • 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:

Fallback content. Fallback content.
Before Fallback content. Inbetween Fallback content. After

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.


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:

Server rendered HTML.

And on our page we’ll use it twice to show how it scales:

Fallback content. Fallback content.

This is how the above template renders:

Server rendered HTML. Server rendered HTML.
Before Server rendered HTML. Inbetween Server rendered HTML. After

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 Server rendered HTML


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 IndieWeb Avatar for Suzanne is leading the charge!

Learn more:

Keep an eye on this method—it’s exciting!

< Newer
Stanford WebCamp 2023
Older >
Defaulting on Single Page Applications (SPA)

Zach Leatherman IndieWeb Avatar for a builder for the web and the creator/maintainer of IndieWeb Avatar for https://www.11ty.devEleventy (11ty), an award-winning open source site generator. At one point he became entirely too fixated on web fonts. He has given 81 talks in nine different countries at events like Beyond Tellerrand, Smashing Conference, Jamstack Conf, CSSConf, and The White House. Formerly part of CloudCannon, Netlify, Filament Group, NEJS CONF, and NebraskaJS. Learn more about Zach »


IndieWeb Avatar for https://www.alvinashcraft.comIndieWeb Avatar for https://som2nypost.comIndieWeb Avatar for https://dinezh.comIndieWeb Avatar for https://soatdev.comBruce B AndersonLea RosemaLoraine LawsonZach Leatherman :11ty:Rob WoodRoderick GadellaawestbrookJoe Gaffey~/j4v1Hubert SablonnièreThomas Steiner :chrome:Daniel Beutner 🦄sombriksEl Perro NegroLene SaileEvanEleventy 🎈 v2.0.1pgrucza@toot.cafeKeith J Grant


Tyler StickajadonnJeffPaul MasontziLea RosemaRob WooddovydenRoderick GadellaawestbrookJoe GaffeySha RazeekRafatehwill@indieweb.socialFlorian GeierstangerHubert SablonnièreNathan BowersTanner HodgesRustieEl Perro NegroMayankVadim MakeevColinautEvanweltEleventy 🎈 v2.0.1Dave RupertRicky de Laveaga
  1. Stuart Langridge

    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

  2. Zach Leatherman :11ty:

    Zach Leatherman :11ty:

    @sil Good feedback, thank you!

  3. 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

  4. Zach Leatherman :11ty:

    Zach Leatherman :11ty:

    @hi_mayank *yes* that will be excellent! cc @mia

  5. 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

  6. 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

  7. westbrook


    @hi_mayank @zachleat is this not what the next level of shadow DOM is for!?! 🤪

Shamelessly plug your related post

These are webmentions via the IndieWeb and

Sharing on social media?

This is what will show up when you share this post on Social Media:

How did you do this? I automated my Open Graph images. (Peer behind the curtain at the test page)