8

I'm learning web components. When designing a custom element, I have to decide what is going to be hidden in the the shadow DOM. The remainder will then be exposed in the light DOM.

As far as I understand, the APIs allow two extreme use cases with different tradeoffs:

  • hide almost nothing in the shadow DOM, most of the element's content is in the light DOM and in the element's attributes:
    • this allows an HTML author to provide anything for the component to display without writing JS;
    • this is close to the status quo regarding searchability and accessibility
    • but there is little reward for the work involved; I add complexity with components but they don't encapsulate anything (everything is exposed).
  • hide almost everything in the shadow DOM, the element's innerHTML is empty:
    • this requires the element to be instantiated from JS;
    • this locks usage a lot more because instantiating from JS is more strict (type-wise) than using HTML slots and attributes;
    • this may be less searchable and accessible (I'm not sure whether this is the case);

I currently lean toward hiding everything in the shadow DOM for the following reasons:

  • I intend to instantiate everything from JS. I'm not going to author pages in HTML manually. It would be more work to code both an HTML API and a JS API.
  • It's less cognitive work to hide everything. I don't need to find a right balance about which information is visible in the light DOM.
  • It's closer to most JS frameworks I'm familiar with.

Am I missing something?


Edit

Thank you, I am answered that it depends on the use case which partially answers my question. But I'm still missing an answer regarding the case I'm in: I'd rather not support slots for some of my components.

I'll add an example for each extreme of the spectrum:

  • Light-DOM-heavy component: the component user has to insert elements into slots
    <template id=light-email-view>
      <div>
        <div><slot name=from></slot></div>
        <ul><slot name=to></slot></ul>
        <h1><slot name=subject></slot></h1>
        <div><slot name=content></slot></div>
        <ul><slot name=attachements></slot></ul>
        <div class=zero-attachment-fallback>no attachments</div>
      </div>
    </template>
    

  • Shadow-DOM-heavy component: the component user has to use the JS API
    <template id=shadow-email-view>
      <div></div>
    </template>
    <script>
    ...
    let view = document.createElement('shadow-email-view');
    // this method renders the email in the shadow DOM entirely
    view.renderFromOject(email);
    container.appendChild(view);
    </script>
    

    In the first example, the component author has more work to do since they need to "parse" the DOM: they have to count attachments to toggle the fallback; basically, any transformation of input that isn't the browser copying an element from the light DOM into the matching shadow DOM slot. Then they need to listen for attribute changes and whatnot. The component user also has more work, they have to insert the right elements into the right slots, some of them non-trivial (the email content may have to be linkified).

    In the second example, the component author doesn't need to implement support for instantiating from HTML with slots. But the component user has to instantiate from JS. All the rendering is done in the .renderFromObject method by the component author. Some additional methods provide hooks to update the view if needed.

    One may advocate for a middle ground by having the component offer both slots and JS helpers to fill those. But I don't see the point if the component isn't to be used by HTML authors and that's still more work.

    So, is putting everything with the shadow DOM viable or should I provide slots because not doing so isn't standard compliant and my code is going to break on some user agent expecting them (ignoring older UAs that are not at all aware of custom elements)?

  • Supersharp
    • 29,002
    • 9
    • 92
    • 134
    guillaume
    • 1,380
    • 10
    • 15
    • 2
      There is no *principled* way to answer this, it depends on the element. You as the *component author* should not be putting elements in light dom inside your component. You *may* offer slots for expected children. – Jared Smith Jan 22 '19 at 02:33
    • That makes a lot of sense to me. But are you using "may" in the normative way, meaning the component author can't always be expected to provide HTML authoring through slots? If so, do you have a source on this? – guillaume Jan 22 '19 at 13:30
    • Think about it, there are times when you don't *want* children to be rendered because it would break your component visually (e.g. button, input, etc). In fact, I'll go out on a limb and say *most* web components are not going to want to render arbitrary children (with the obvious exception of lists and other containers). Generally though, you as the component author won't want to put anything in light DOM because it breaks the encapsulation, which is a big part of the whole point of using web components. – Jared Smith Jan 22 '19 at 15:41
    • Yes, that's one of my issues with slots: I don't want to accept random input most of the time. So you're saying I can make components without slots that only accept child content from JS function calls? – guillaume Jan 22 '19 at 15:53
    • I think you mistake me. Slots have nothing to do with JS function calls. You can provide named slots that your users place elements into via the `slot` attribute or you can provide a more generic slot in your template. If you only want *specific* children you do the former, if you'll accept arbitrary children the latter. I mean, yeah sure, you can provide a custom method that appends to the shadow root, but that's... weird. Just use named slots, that's what they're there for. And your users can always append arbitrary children to light DOM, they just won't be rendered. – Jared Smith Jan 22 '19 at 16:07
    • I'm asking for the weird case actually. I've edited the question text to make it more explicit. – guillaume Jan 22 '19 at 23:56

    4 Answers4

    7

    The choice is 100% dependent on the use case.

    Also:

    • if you want the user to be able to format your custom element with global CSS style attributes, you may opt for the normal, light DOM.

    • you're right: in the Shadow DOM, "this may be less searchable": the document.querySelector() method won't inspect the Shadow DOM content.

    • as a consequence, some third-pary JS library may fail to integrate easily with Shadow DOM

    • if you intend to use a Custom Element polyfill for legacy browsers, you may avoid Shadow DOM because some of its features cannot be really polyfilled.

    • in many cases, the answer is to provide a mix of Light DOM and Shadow DOM. As suggested by @JaredSmith:

      • Shadow DOM for the Web Component author,
      • Light DOM for the Web Compoent user, intergrated in the Shadow DOM with <slot>.

    As a conclusion, you should consider the context in which your Web Component will be used to decide whether Shadow DOM is required or not.


    Answer to the Edit

    Considering your use case, I would create a custom element and:

    • let the user populate the light DOM with atomic value(s): type element <div class="mail-to"> or custom sub-components <mail-to> as suggested by @Intervalia,
    • use a Shadow DOM to mask the light DOM,
    • use Javascript: this.querySelectorAll('.mail-to') or this.querySelectorAll('mail-to') instead of <slot> to extract data from the light DOM and copy (or move) them to the Shadow DOM.

    This way users won't have to learn the <slot> working, and the developer will be able to format the web component rendering with more freedom.

    <email-view>
      <mail-to>guillaume@stackoverflow.com</mail-to>
      <mail-to>joe@google.fr</mail-to>
      <mail-from>supersharp@cyber-nation.fr</mail-from>
      <mail-body>hello world!</mail-body>
    <email-view>
    
    Supersharp
    • 29,002
    • 9
    • 92
    • 134
    • CSS is a good point. Searchability was meant as SEO friendly. Integration with third-party JS is better in shadow DOM in my view as it avoids someone messing with the internals. Polyfill-ability is a good point. – guillaume Jan 22 '19 at 09:50
    • Yes every alternative can be seen as good or bad depending on the goals. About SEO: it depends how it is implemented by the SEO but it could fail to inspect the Shadow DOM content if it uses querySelector or similar standard DOM treewalk for its parsing. – Supersharp Jan 22 '19 at 10:05
    • 1
      Actually your last sentence raises another good point, shadow dom at least atm adds a fairly hefty performance penalty. That's no reason not to use it if it makes sense, but not the sort of thing I'd do gratuitously. – Jared Smith Jan 22 '19 at 15:38
    • @JaredSmith yes it you deal with thousands of custom elements in the same page you should evaluate the Shadow DOM overload. – Supersharp Jan 22 '19 at 15:59
    • Thank you for editing your answer. Honestly, in my case where I'm excluding HTML authors, I don't see the merit to have data flow from JS to HTML back to JS. But my real question is whether one can do custom components with zero light DOM and still be standard compliant. Your example doesn't have slots, so that's almost a yes. Would be better to have a more direct answer, possibly sourced. – guillaume Jan 24 '19 at 01:21
    • For those that care about code validation, automated testing, or WCAG SC 4.1.1 (Parsing) there are some attributes which are invalid if they are used on the wrong element type, (or have the wrong aria role). Components have no default semantic role. If a component contains an `HTMLImageElement` in its shadow dom, it's (semantically speaking) not an image, and it would be invalid to put an `alt` attribute on the component wrapper, despite the image inside. Exposing `role="img"` or adding it to the component instance would allow `aria-label`, at least. – brennanyoung Oct 19 '21 at 12:41
    7

    @supersharp has nailed it.

    One thing I see with Web Components is that people tend to have their component do way too much instead of breaking into smaller components.

    Let's consider some native elements:

    <form> there is no shadow DOM and the only thing it does is read values out of its children form elements to be able to do an HTTP GET, POST, etc.

    <video> 100% shadowDOM and the only thing it uses the app supplied children for is to define what video will be playing. The user can not adjust any CSS for the shadow children of the <video> tag. Nor should they be allowed to. The only thing the <video> tag allows is the ability to hide or show those shadow children. The <audio> tag does the same thing.

    <h1> to <h6> No shadow. All this does is set a default font-size and display the children.

    The <img> tag uses shadow children to display the image and the Alt-Text.

    Like @supersharp has said the use of shadowDOM is based on the element. I would go further to say that shadowDOM should be a well thought out choice. I will add that you need to remember that these are supposed to be components and not apps.

    Yes, you can encapsulate your entire app into one component, but the browsers didn't attempt to do that with Native components. The more specialized you can make your components to more reusable they become.

    Avoid adding anything into your Web Components that is not vanilla JS, in other words, do not add any framework code into your components unless you never want to share them with someone that does not use that framework. The components I write are 100% Vanilla JS and no CSS frameworks. And they are used in Angular, React and vue with no changes to the code.

    But chose the use of shadowDOM for each component written. And, if you must work in a browser that does not natively support Web Components that you may not want to use shadowDOM at all.

    One last thing. If you write a component that does not use shadowDOM but it has CSS then you have to be careful where you place the CSS since your component might be placed into someone else's shadowDOM. If your CSS was placed in the <head> tag then it will fail inside the other shadowDOM. I use this code to prevent that problem:

    function setCss(el, styleEl) {
      let comp = (styleEl instanceof DocumentFragment ? styleEl.querySelector('style') : styleEl).getAttribute('component');
      if (!comp) {
        throw new Error('Your `<style>` tag must set the attribute `component` to the component name. (Like: `<style component="my-element">`)');
      }
    
      let doc = document.head; // If shadow DOM isn't supported place the CSS in `<head>`
      // istanbul ignore else
      if (el.getRootNode) {
        doc = el.getRootNode();
        // istanbul ignore else
        if (doc === document) {
          doc = document.head;
        }
      }
    
      // istanbul ignore else
      if (!doc.querySelector(`style[component="${comp}"]`)) {
        doc.appendChild(styleEl.cloneNode(true));
      }
    }
    
    export default setCss;
    
    Intervalia
    • 10,248
    • 2
    • 30
    • 60
    • 1
      Interesting thoughts that are all the more valid as one tries to build more reusable components. But could you be more specific than "@supersharp nailed it": is a component author expected (by browsers/screen readers/search engines...) to support an HTML API (with slots)? I can see why one would think it's a good thing but I believe sometimes it's justified to make a JS-only API (class methods). Am I going to run into trouble? – guillaume Jan 22 '19 at 15:45
    • My use of "@supersharp nailed it" indicated that I agreed with his response especially the statement of "The choice is 100% dependent on the use case." – Intervalia Jan 22 '19 at 16:18
    4

    Alright. Setting aside for a moment that I think this is a bad questionable idea, here's some code that should do what you want (I didn't run it, but it should work):

    class FooElement extends HTMLElement {
      constructor () {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.appendChild(document.importNode(template.content, true));
      }
    
      _xformObject (object) {
        // turn the obj into DOM nodes
      }
    
      renderFromObject (object) {
        // you may need to do something fancier than appendChild,
        // you can always query the shadowRoot and insert it at
        // a specific point in shadow DOM
        this.shadowRoot.appendChild(this._xformObject(object));
      }
    }
    

    You'll have to register the custom element of course.

    Now sometimes you really can't get away from doing something like this. But it should be the absolute last resort. See below:

    Why I think this is a bad questionable idea, and how to make it better:

    One of the main draws to web components is that it enables declarative HTML markup rather than procedural JS DOM manipulations. While providing an API like what you're talking about is certainly a big step up from e.g. creating a table by creating a table node, creating a row node, creating some tds, appending them to the row, then appending that to the table, I (and I think most) developers are of the idea that if your custom element requires direct JavaScript manipulation by the user, then it's not really an HTML element: it's a JavaScript interface.

    Let me qualify that a little. When I say "requires" JavaScript I mean there's no way to drop it on the page with some appropriate attributes and wind up with the thing you want. When I say "direct" I mean by calling methods of the object representation of the element directly rather than say toggling an element attribute. To put my point in code:

    // good
    myCustomElement.setAttribute("toggled-on", true);
    
    // this isn't *bad*, but don't *force* people to do this
    myCustomElement.toggleState();
    

    You may want to still provide the second as part of your public API as a convenience to your users, but requiring it seems beyond the pale. Now one issue is that you obviously can't easily pass complex data structures to an HTML attribute (Polymer has helpers for this if you're using Polymer).

    But if that's the case, rather than have that be part of the element API, I'd provide a standalone function that returns the appropriate DOM structure rather than baking that in to an element. You could even make it a class method of your custom element class if that's how you roll.

    Consider the case where you have a List element that renders an arbitrary number of Item elements. I think it's great to provide a convenience method that takes an array and updates the (light) DOM. But users should be able to append them directly as well.

    Your use case may require hacking around the problem. Sometimes you really do legit need to use an antipattern. But consider carefully whether that's the case in your element.

    Jared Smith
    • 19,721
    • 5
    • 45
    • 83
    • Thank you for your thorough answer. You do make valid points but I was more asking whether to support slots for technical requirements rather than developer preferences. I want to know because my personal preference is JS (or transpiled languages) rather than HTML. In most cases, I'll be the only component author and component user, so there is no point pleasing others. – guillaume Jan 23 '19 at 15:51
    • @guillaume I mean, that's cool and all, but then why bother with custom elements in the first place? You could just make a bunch of vanilla JS that returns normal DOM, use very specific CSS classes for styling, etc. Plenty of component libraries use that approach, even in today's post-React world. You're taking on all of the headaches of web components (mediocre portability, performance issues, interop issues, boilerplate, more complicated build pipeline) for seemingly little benefit. That's why your scenario feels off to me, for what you want I wouldn't bother with custom elements at all – Jared Smith Jan 23 '19 at 16:14
    • The killer feature for me is CSS scopes. Then having less specific classes and short IDs is nice. Lastly, having a very light framework is interesting: no preprocessing; vanilla components that could last longer. Overall, that could make something more maintainable. Anyway, custom components may turn out to be a fad but I figured I should try them. I'm only trying to check whether my own style is standard compliant. – guillaume Jan 23 '19 at 16:53
    • @guillaume I don't think you're writing anything that will necessarily break in the future, but I don't know if shorter CSS selectors is worth the headache, especially since there are some pretty robust CSS-in-JS solutions at this point. – Jared Smith Jan 23 '19 at 18:27
    1

    This is purely dependent on your case. But as a general rule, if you find yourself buried in a hell of nested shadow roots, then you may consider going easy on using shadow doms. Like the follow example illustrates:

    <my-outer-element>
         shadowRoot
           <slot1> ---Reveal dom
           <my-inner-element>
              shadowRoot
                    ....