A brief introduction to Web Components

Templates & Slots

This article explains how you can use the <template> and <slot> elements to create a flexible template that can then be used to populate the shadow DOM of a web component.

The <template> Element

The <template> element allows you to declare fragments of the DOM which are parsed, inserted at page load, and can be activated later at runtime. Think of a template as a content fragment that is being stored for subsequent use in the document. While the parser does process the contents of the <template> element while loading the page, it does so only to ensure that those contents are valid; the element's contents are not rendered, however. Templates are an ideal placeholder for declaring the structure of a custom element.

Browser support

Data on support for the HTML template feature across the major browsers from caniuse.com

Usage without other Web Components technology

HTML templates can of course be used independently. In this simple use case, we use templates to display the same data in different structures and layouts.

See the Pen Show data differently with HTML templates by Joerg Schneider (@byjs-dev) on CodePen.

The versatility of HTML templates

One of the interesting things about templates is that they can contain any HTML. That includes script and style elements. A very simple example would be a template that appends a button that alerts us when it is clicked.

So in practice we could do this:
                    
<template id="template">
    <script>
        const tpl_button = document.querySelector('.zep-button');
        tpl_button.addEventListener('click', event => alert(event));
    </script>
    <style>
        .zep-button {
            all: unset;
            background-color: rgb(255, 110, 0);
            border: 0;
            border-radius: 0.25rem;
            color: white;
            font-family: 'Roboto', sans-serif;
            font-size: 1rem;
            padding: .5rem 1rem;
            text-transform: uppercase;
        }
    </style>
    <button class="zep-button">Click me</button>
</template>
                    
                

See the Pen Simple <template> example by Joerg Schneider (@byjs-dev) on CodePen.

But please don't do it that way in the standard DOM. Why? Because the style tag inside the body is invalid and it's also not scoped to the content of your template, neither is the script. That means everything inside your template will effect the global scope. However, when using the shadow DOM, things are quite different! Then it makes perfect sense because your content is isolated. We'll get to that later...

Shadow DOM composition using slots

Composition is how we construct apps. Different building blocks (<div>, <header>, <form>, <input>) come together to form them. Some of these tags even work with each other. Composition is why native elements like <select>, <details>, <form>, and <video> are so flexible. Each of those tags accepts certain HTML as children and does something special with them. For example, <select> knows how to render <option> and <optgroup> into dropdown and multi-select widgets.

Shadow DOM composition introduces a bunch of new fundamentals in web development. Let's standardize on some terminology.

Light DOM

The markup a user of your component writes. This DOM lives outside the component's shadow DOM. It is the element's actual children.
                    
<zep-button>
    <!-- the image and span are zep-button's light DOM -->
    <img src="triangle.svg" alt="A vector graphic showing a triangle" slot="icon">
    <span>Triangle</span>
</zep-button>
                    
                

Shadow DOM

The DOM a component author writes. Shadow DOM is local to the component and defines its internal structure, scoped CSS, and encapsulates implementation details. It can also define how to render markup that's authored by the consumer of the component.
                    
#shadow-root
    <style>...</style>
    <slot name="icon"></slot>
    <span id="wrapper">
        <slot>Button</slot>
    </span>
                    
                

Flattened DOM tree

The result of the browser distributing the user's light DOM into your shadow DOM, rendering the final product. The flattened tree is what you ultimately see in the DevTools and what's rendered on the page.
                    
<zep-button>
    #shadow-root
    <style>...</style>
    <slot name="icon">
        <img src="triangle.svg" alt="A vector graphic showing a triangle" slot="icon">
    </slot>
    <span id="wrapper">
        <slot>
            <span>Triangle</span>
        </slot>
    </span>
</zep-button>
                    
                

The <slot> Element

Slots are placeholders inside your component that users can fill with their own markup. Elements are allowed to "cross" the shadow DOM boundary when a <slot> invites them in. Slots don't physically move DOM; they render it at another location inside the shadow DOM.

Slots are a way of creating a "declarative API" for a web component. They mix-in the user's DOM to help render the overall component, thus, composing different DOM trees together. For those of you who remember AngularJS, this is a similar concept to ng-transclude or using props.children in React.

Example with a Tabs component

Shadow DOM of a fictional <zep-tabs> component
                    
#shadow-root
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot> <!-- named slot -->
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>
                    
                
Component users declares <zep-tabs> like so:
                    
<zep-tabs>
    <button slot="title">Title</button>
    <button slot="title" selected>Title 2</button>
    <button slot="title">Title 3</button>
    <section>content panel 1</section>
    <section>content panel 2</section>
    <section>content panel 3</section>
</zep-tabs>

<!-- Using <h2>'s and changing the ordering would also work! -->
<zep-tabs>
    <h2 slot="title">Title</h2>
    <section>content panel 1</section>
    <h2 slot="title" selected>Title 2</h2>
    <section>content panel 2</section>
    <h2 slot="title">Title 3</h2>
    <section>content panel 3</section>
</zep-tabs>
                    
                
The flattened tree looks something like this:
                    
<zep-tabs>
    #shadow-root
    <div id="tabs">
        <slot id="tabsSlot" name="title">
        <button slot="title">Title</button>
        <button slot="title" selected>Title 2</button>
        <button slot="title">Title 3</button>
        </slot>
    </div>
    <div id="tabs">
        <slot id="panelsSlot">
        <section>content panel 1</section>
        <section>content panel 2</section>
        <section>content panel 3</section>
        </slot>
    </div>
</zep-tabs>
                    
                

The component is able to handle different configurations, but the flattened DOM tree remains the same. We can also switch from <button> to <h2>. This component was authored to handle different types of children… just like <select> does!

Styling Custom Elements

follow https://web.dev/shadowdom-v1/#styling
and use https://zepdev.github.io/web-components-library-gh-pages/?path=/docs/components-zbutton--default
as an example

Custom Elements - Accessibility

to be continued... (pitfalls and how to avoid them -> using slots, form-associated custom elements, etc.)