Light-DOM-Only Web Components are Sweet

First: the Light DOM is just… the regular DOM.

When people talk about native Web Components, the Shadow DOM comes up a lot. I have extremely mixed feelings about the Shadow DOM. On one hand, it’s a powerful scoping tool. For example, CSS applied inside the Shadow DOM doesn’t “leak” outside, meaning you can be freer with naming and not worry about selector conflicts. Also, JavaScript from the outside can’t reach in, meaning a querySelectorAll isn’t going to select things inside the Shadow DOM. It’s a protective barrier that is unique to Web Components. No library can offer this, and that’s cool.

That’s what the Shadow DOM is. What it isn’t is required for “HTML abstraction”, which I feel like it gets confused for. It had me confused for a while, anyway. If one of your goals for a Web Component is to hide away the HTML implementation details of a component, perhaps for ease-of-use, well, that’s great, but you can do that with the Shadow DOM or the Light DOM.

Let’s get into this.

The Guts of a Custom Element Can Become Light DOM

Here’s a custom element defined as alert-machine:

<alert-machine>
  <button>I'm a button.</button>
</alert-machine>Code language: HTML, XML (xml)

In order for that to do anything, we need to execute some JavaScript that defines that element and its functionality:

class AlertMachine extends HTMLElement {
  connectedCallback() {
    this.querySelector("button").addEventListener("click", () => {
      alert("Alert machine strikes again.");
    });
  }
}

customElements.define("alert-machine", AlertMachine);Code language: JavaScript (javascript)

I kinda prefer the slightly more elaborate setup with a constructor that allows for methods and such:

class AlertMachine extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    this.querySelector("button").addEventListener("click", this.doAlert);
  }

  doAlert() {
    alert("Alert machine strikes again.");
  }
}

customElements.define("alert-machine", AlertMachine);Code language: JavaScript (javascript)

Even if this is the first time you’re ever seeing Web Components code like this, you can make sense of it.

What I like about what is happening so far is that <button> in the HTML is going to render on the page just like… a… <button> in HTML. It’s 100% just like that:

  1. It renders like a <button>
  2. You can select it and style it from regular CSS with button { }
  3. You can querySelector it from regular JavaScript

It’s no different than any other HTML on the page.

But notice the JavaScript inside the Web Component is adding a little extra functionality. That’s it. Neat.

You Can Augment or Replace the HTML with Whatever

At this point, you might think:

  1. OK, whatever is inside the custom element is the Light DOM. And:
  2. Well, that’s pretty limiting. I don’t want to have to be in charge of adding in all the needed HTML for every single component. That doesn’t bring very much value.

That’s kinda true, but it doesn’t have to be the entire truth. You can totally wipe that away and inject your own HTML. You could keep that HTML but add more, wrapping things or whatever you need to do. Or you could just keep it as is. This is really no different than the possibilities with Shadow DOM. Do what you gotta do. You do lose the ability to use <slot />, which is pretty unfortunate, and I wish wasn’t true, but alas.

So if your goal is to abstract away a bunch of HTML to make your component easier to use, go for it!

But you probably should get on board with delivering a good set of HTML within your custom elements right in the delivered HTML. For one, that’s the fallback when JavaScript fails to load or run, which matters. But it’s not just a “fallback” or progressive enhancement technique entirely, it’s what is required for SSR (server-side rendering) which is also a performance concern and I think we all generally agree is a good idea.

While I think the possibility that not having a build process for Web Components is attractive, if what the build process buys you is good, solid, HTML, then it’s probably worth it. (I’m mostly thinking of WebC and Enhance here).

<my-component>
  <div class="card">
    <h2>Good, solid, renderable, accessible, non-embarassing HTML in here.</h2>
  <div>
</my-component>Code language: HTML, XML (xml)

Take, for example, the Image Comparison Slider that Cloud Four put out as a Web Component:

<image-compare>
  <img alt="Alt Text" src="path/to/image.jpg" />
  <img alt="Alt text" src="path/to/image.jpg" />
</image-compare>Code language: HTML, XML (xml)

Just two images! That’s a perfect fallback for this, because two images are exactly what you’re trying to show. But when the comparison slider loads, it’s got all sorts of other HTML that make it do the interactive stuff. That’s perfect.

Their slider does use the Shadow DOM, which is fine of course. And actually, they use <slot /> to put the images into place in the abstracted HTML is pretty useful. But they didn’t have to, this all could be done in the Light DOM. It would be a little extra work producing the final HTML, but a hell of a lot easier for consumers to style.

Jim Nielsen has a nice way of saying it:

This feature of web components encourages a design of composability. Rather than an empty “shell component” that takes data and (using JavaScript exclusively) renders the entirety of its contents, web components encourage an approach of composing core content with HTML and then wrapping it in a custom element that enhances its contents with additional functionality.

I don’t like styling the Shadow DOM

That’s kind of the rub here, for me. The main reason I’m so hot on Light DOM is that I find the styling story of Web Components using Shadow DOM annoying.

  • Styling very specific things with ::part is a very niche styling thing and to me, not a real styling solution.
  • Styling by documenting somewhere that certain CSS --custom-properties are in use is also limited and not a real styling solution.
  • Styling by injecting a <style> tag into some template literal in the JavaScript itself feels awkward and ad hoc to me and I’m not a fan.
  • Styling with an adopted stylesheet means an additional web request for each component or back to the template literal thing which is either awkward or slow.

I don’t dislike that these options exist, I just don’t… like them. I’d rather be able to use the best styling API ever: regular CSS. There is some hope here, the idea of “open stylable shadow roots” might take hold.

So what are we giving up again?

If you go all-Light-DOM with a Web Component, you lose:

  • Encapsulation
  • Slots

You gain the ability to use regular ol’ CSS from the parent page to style the thing. And, perhaps, a stronger reminder that Web Components should have good default HTML.

What People Are Saying About Light DOM

They certainly resonate with Eric Meyer!

like the Light DOM.  It’s designed to work together pretty well.  This whole high-fantasy-flavored Shadowlands of the DOM thing just doesn’t sit right with me.

If they do for you, that’s great!  Rock on with your bad self.  I say all this mostly to set the stage for why I only recently had a breakthrough using web components, and now I quite like them.  But not the shadow kind.  I’m talking about Fully Light-DOM Components here.

Blinded By the Light DOM

Some baby bear just-right porridge from Jeremy Keith:

Dave talks about how web components can be HTML with superpowers. I think that’s a good attitude to have. Instead of all-singing, all-dancing web components, it feels a lot more elegant to use web components to augment your existing markup with just enough extra behaviour.

Where does the shadow DOM come into all of this? It doesn’t. And that’s okay. I’m not saying it should be avoided completely, but it should be a last resort. See how far you can get with the composibility of regular HTML first.

HTML web components

Mayank has a pretty hardline stance, and gets into similar problems I have with styling.

I’ve previously said “shadow DOM is not fit for production use”, a statement which attracted a surprising amount of heat. Maybe I’m asking for too much, but I would think that every respectable production-grade application has core needs — like accessibility, form participation, and the ability to work without JavaScript.

Today though, I want to touch a little bit on the styling side of things.

Presentational shadow DOM

Jim Neilsen used Light DOM only, found it useful, and even felt weird about it (which you should not, Jim)!

Maybe I shouldn’t be using the term “web component” for what I’ve done here. I’m not using shadow DOM. I’m not using the templates or slots. I’m really only using custom elements to attach functionality to a specific kind of component.

But it still kinda feels like web components. All of this could’ve been accomplished with regular ole’ web techniques

Using Web Components on My Icon Galleries Websites

Adam Stoddard is into it:

No shadow DOM, no templates, just the regular old DOM, which we now get to call the much cooler sounding “light DOM”.

Adam specifically call out how cool the connectedCallback is. Whenever a custom element appears in the DOM it essentially auto-instantiates itself, which is an “unsung hero” of Web Components. Dave Rupert’s simple and useful <fit-vids> has no Shadow DOM in sight, it just applies a few styles to what it finds in the Light DOM, but another reason to use it is how it automatically applies the styles when it shows up in the DOM. If you were to use the old school fitvids.js library, you would have to re-call the library if new videos were injected after it ran the first time.

I’ll end with Miriam Suzanne:

Let the light DOM handle content wherever possible.

HTML Web Components are Just JavaScript?

How about y’all? How you feeling about the Light DOM approach?

5 responses to “Light-DOM-Only Web Components are Sweet”

  1. you can duplicate slot functionality with a single utility function. heres an example:

    class NewComponent extends HTMLElement {
      connectedCallback() {
        const html = `
        <header>
          <slot name="hdr"></slot>
        </header>
        <main>
          <slot name="mn"></slot>
        </main>
        <footer>
          <slot name="ftr"></slot>
        </footer>`;
        this.childrenToSlots(html);
      }
    
      childrenToSlots(html, src_elems) {
        var template = document.createElement("template");
        template.innerHTML = html;
    
        const slots = template.content.querySelectorAll("slot");
        for (const slot of slots) {
          const slotChildren = this.querySelectorAll(`[slot='${slot.name}']`);
          slot.replaceWith(...slotChildren);
        }
    
        this.replaceChildren(template.content);
      }
    }
    customElements.define("new-comp", NewComponent);
    

    https://jsfiddle.net/f5pr21ju/

  2. Alas, your code only works in CodePens and JSFiddles because those execute the JavaScript after the DOM is parsed.
    IRL Custom Elements can be defined before DOM is parsed, and since the connectedCallback runs on the Opening Tag… there won’t be any lightDOM .. yet..

  3. Shadow-DOM really does make styling terrible. You can pierce through with CSS vars but that means the web components have to be set up to allow theming with CSS vars. So color, type, shape, spacing/padding, all have to be CSS vars if you want to allow someone to theme. Using tailwind with Shadow-DOM web components is also really hard, mainly due to the fact you can’t allow any dynamic variants in styling without being explicit about the options. I really do like the idea of web components though, I’ll have to give Light-DOM a go.

    You mentioned a benefit for Shadow-DOM as encapsulation (which I’ve used before as well) but really, should there be so much JS on your page that you have collisions? JS has just gotten way out of hand. Coming from the massive JS framework perspective you automatically think, “of course you should encapsulate” but really? Do you?

    Thanks, great article.

  4. Avatar John Loy says:

    Thanks Chris!

    The thoughts and approaches mentioned in this article dovetail nicely with those mentioned in https://jordanbrennan.hashnode.dev/tac-a-new-css-methodology, by the way.

    Custom HTML tags sans-Shadow DOM make a ton of sense for app/site-specific “components” like page skeletons, layouts, idiosyncratic UI, and wrappers/customizations around 3rd-party components. At least, they make sense if you care deeply about your markup structure, SEO, A11y, Web Vitals like LCP and CLS, JS payload size, and progressive enhancement.

    The Shadow DOM and its encapsulation capability, however, seems very appropriate and useful for packaged 3rd-party components as a way for component authors to control the component public API in terms of behavior, content (slots), and styles. Take, for example, https://shoelace.style/. It’s a great example of customization in tandem with the Shadow DOM through features like slots, CSS custom properties and CSS parts. If these sorts of components could be customized without limits, then the ways a consumer could break them would wreak havoc on maintenance and support. The same applies, I think, to a well-maintained component collection for an internal design system, like https://opensource.adobe.com/spectrum-web-components/.

    In my experience, where folks tend to go sour on web components is trying to wrangle with how to use Shadow DOM for app-specific components. They end up discovering that they can’t use global styles, or their sacred Tailwind cow, querySelector doesn’t seem to work as they expect, and they’re like, “what’s the point of making things harder”, and they totally write off web components as a technology because XYZ popular framework doesn’t have these constraints.

    The custom element aspect of web components, of course, doesn’t require using the Shadow DOM–you still get JS “upgrading” and sweet-looking CSS selectors for free–so I’m glad to hear folks are coming around more and more to using them for this.

Leave a Reply

Your email address will not be published. Required fields are marked *