Web Components

Bernardo Cardoso
14 min readJun 22, 2020

--

The Pillars of Creation

The Pillars of Creation, taken by the Hubble telescope in 1995.

Introduction

Web Components are a new browser feature, allowing you to create reusable custom HTML elements — with their functionality encapsulated away from the rest of your code — and utilize them across your projects.

In reality, it’s not that new, it has been around since 2013 or so. However, recently had some major improvements and a lot more visibility, especially through Google. The overall browser support is getting very stable and the gaps are filled with working polyfills.

Simplified Information. See full table here.

To avoid confusions from the start, it can be useful to define what, technically, are Web Components:

  • Low Level Browser API’s
  • Standard Component Interface

But even more important, is to highlight what Web Components aren’t:

  • A Framework
  • A rendering library

The Four Pillars of Creation

Some of you probably already vaguely heard about the template tag or the existence of the Shadow DOM, but can’t exactly explain what they are or their purpose.

When talking about Web Components, we are really talking about four different features:

Each one of these can work separately from one another, but it’s when used together that they form what’s called a Web Component, and are considered part of the whole specification.

So, let’s dive in more detail about each one!

HTML Templates

There are user-defined templates in HTML that aren’t rendered until called upon. In other words, a template is HTML that the browser ignores until told to do otherwise.

Running the code above in a browser would result in an empty screen as the browser doesn’t render the template element’s contents. This becomes incredibly powerful because it allows us to define content (or a content structure) and save it for later — instead of writing HTML in JavaScript. They can also contain any HTML — that includes script and style elements.

The real magic happens in the document.importNode method. This function will create a copy of the template’s content and prepare it to be inserted into another document (allowing multiple instances of the same template)

  • The first argument to the function grabs the template’s content
  • The second argument tells the browser to do a deep copy of the element’s DOM subtree (i.e. all of its children).

Custom Elements

With Custom Elements, web developers can create new HTML tags, beef-up existing HTML tags, or extend the components other developers have authored. The API is the foundation of web components. It brings a web standards-based way to create reusable components using nothing more than vanilla JS/HTML/CSS. The result is less code, more modular and reusable, in our apps.

You may be asking: “but I can already do that, I can use almost any syntax on HTML tags. What’s the difference? Why don’t just use a normal div?”

The difference is that by creating a Custom Element, you aren’t just creating a static HTML tag, you’re creating a brand new and personalized HTML element with the whole DOM API available.

In the following example, you can see the creation of a osui-card custom element (Note: I will be using the osui name convention, as it stands for OutSystems UI Framework, the project I work on at OutSystems. You should pick a naming convention that makes sense in your project).

The customElements global is used for defining a custom element and teaching the browser about a new tag. Call customElements.define() with the tag name you want to create and a JavaScript class that extends the base HTMLElement.

Rules on creating custom elements:

  • The name of a custom element must contain a dash (-). This requirement is so the HTML parser can distinguish custom elements from regular elements. It also ensures forward compatibility when new tags are added to HTML.
  • You can’t register the same tag more than once. Attempting to do so will throw a DOMException.
  • Custom elements cannot be self-closing, because HTML only allows a few elements to be self-closing.

Defining an element’s JavaScript API

As mentioned above, extending HTMLElement ensures the custom element inherits the entire DOM API and means any properties/methods that you add to the class become part of the element’s DOM interface. Essentially, use the class to create a public JavaScript API for your tag!

A neat feature of custom elements is that this inside a class definition refers to the DOM element itself i.e. the instance of the class. The entire DOM API is available inside the element code.

Custom element reactions

A custom element can also define special synchronous lifecycle hooks for running code during interesting times of its existence. These are called custom element reactions, and work similarly to react’s Lifecycle events.

  • connectedCallback — Called every time the element is inserted into the DOM. Useful for running setup code, such as fetching resources or rendering.
  • disconnectedCallback — Called every time the element is removed from the DOM. Useful for running clean up code.
  • adoptedCallback — Invoked each time the custom element is moved to a new document.
  • attributeChangedCallback — Invoked each time one of the custom element’s attributes is added, removed, or changed.

Extending a custom element

The Custom Elements API is useful for creating new HTML elements, but it’s also useful for extending other custom elements or even the browser’s built-in HTML.

In this example, we are extending our existing card component, and using it as the base for the CardSectioned. Most of the code would be the same, we just want to really add some more elements and Flexbox options.

Extending native HTML elements

A customized built-in element is a custom element that extends one of the browser’s built-in HTML tags.

So, with this we can create our own button and use that to add some custom behavior by default, like a hover animation or a space for a icon, while keeping all of its native features (DOM properties, methods, accessibility).

You just need to extend the HTMLButtonElement itself, in javascript, and define it as before. The required third parameter tells the browser which tag you’re extending.

The difference in HTML is that we don’t use the tag syntax as before, but instead we use the ‘is’ property to tell the browser this button is our custom element.

Check this codepen to toy around extending HTML elements:

Styling a custom element

A custom element can be stylized as any other HTML tag. With the advantage that we have some extra options available. As an example, before an element is defined you can target it in CSS using the :defined pseudo-class. This is useful for pre-styling a component, before the data is available.

Shadow DOM

The shadow DOM is an encapsulated version of the DOM. Authors can effectively isolate DOM fragments from one another and, together with Custom Elements, make a component with self-contained HTML, CSS, and JavaScript.

Here’s some bits of shadow DOM terminology to be aware of:

  • Shadow host: The regular DOM node that the shadow DOM is attached to.
  • Shadow tree: The DOM tree inside the shadow DOM.
  • Shadow boundary: the place where the shadow DOM ends, and the regular DOM begins.
  • Shadow root: The root node of the shadow tree.

DOM Composition — Light & Shadow

Before explaining how to use it, I think it may be useful to do a brief explanation about it’s position on the DOM and how it interacts with it.

DOM Composition is how different building blocks (<div>s, <header>s, <form>s, <input>s) come together to form apps. Some of these tags even work with each other. Composition is why native elements like <select> 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.

The Light DOM is the markup a user of your component writes. This DOM lives outside the component’s shadow DOM. It is the element’s actual children.

The Shadow DOM is the DOM a component author writes. Shadow DOM is local to the component and defines its internal structure, scoped CSS, and encapsulates your implementation details. It can also define how to render markup that’s authored by the consumer of your component.

The Flattened DOM is 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.

Creating a Shadow DOM

A shadow root is a document fragment that gets attached to a “host” element. The act of attaching a shadow root is how the element gains its shadow DOM. To create shadow DOM for an element, call element.attachShadow():

The spec defines a list of elements that can’t host a shadow tree.

Shadow DOM and Custom Elements

Shadow DOM is particularly useful when creating custom elements. Use it to compartmentalize an element’s HTML, CSS, and JS, thus producing a “Web Component”.

Take a more detailed look using this codepen, with the <osui-card> from above:

The <slot> element

Slots are placeholders inside your component that users can fill with their own markup. By defining one or more slots, you invite outside markup to render in your component’s shadow DOM (similar to props.children in React). This is particularly useful if you’re authoring components for others to use.

  • Elements are allowed to “cross” the shadow DOM boundary when a <slot> invites them in.
  • You can also create named slots. Named slots are specific holes in your shadow DOM that users reference by name.

HTML Modules

This feature has a more rough development, and it’s still the least stable from the Web Components. After some deprecations, the current feature is an extension of the ES6 Script Modules system to include HTML Modules. These will allow web developers to package and access declarative content from script in a way that allows for good componentization and reusability, and integrates well into the existing ES6 Modules infrastructure.

Here’s an example of a simple usage, exporting a Web Component:

Using HTML Export

Using HTML Import

So, we already covered the main features of the Web Components specification. However, bare in mind that this was a very summarized description, there’s much more into it! If you want to learn more about this, check some of the references at the end of the article.

Next, let’s focus on some thoughts about how this articulates with the overall frontend architecture, and Web Component’s place among a Design System.

Before the Atom

The idea behind a component is that it should be completely independent, without the need of external code to provide it’s core functionalities. In fact, it should be the most indivisible element in your application.

Having Brad Frost’s Atomic Design in mind, most of you are probably thinking: “Well, that‘s the Atoms, right?”. In my opinion, no. Atomic Design is essentially aimed at the visual identification and organization of UI elements, as well as the relations between them. Not exactly how they are built on the inside.

A button is undeniable an Atom. However, it can in itself also be a custom element, with a series of listeners, methods, etc., that provide different behaviors. It can have a Shadow Dom with a structure for a text and an icon, or just some extra div to help build an animation (just like the ripple effect of Material UI’s button). Having this, it should also contain it’s essential CSS styles needed to work.

That’s already a good amount of potential elements. We need to go a layer deeper.

Fortunately, in 1911, Ernest Rutherford discovered the Atom’s Nucleus — a small, dense region consisting of protons and neutrons at the center of an atom.

That’s a pretty close approach to what Web Components should represent on the overall scheme of a Design System. The minimal, indivisible amount of code, to make it work anywhere, on its own.

As we saw, the four main specifications give you the right tools to achieve this. From one side, Custom Elements makes them browser native, which implies the ability to work everywhere. While, on the other end, Shadow DOM provides protection and reliability, like an ultimate fortress, preventing unwanted code to bleed into your component.

Let There Be Light

With all this talking about Shadow DOM and the code, particularly the styles, being so closed and unaffected by the exterior, how are we supposed to properly style something, following a Design System?

Using Shadow DOM brings a bunch of limitations on how CSS is applied:

  • CSS selectors from the outer page don’t apply inside your component.
  • Styles defined inside don’t bleed out. They’re scoped to the host element.
  • Linked stylesheets are also scoped to the shadow tree.
“It is not despair, for despair is only for those who see the end beyond all doubt. We do not.” (J.R.R. Tolkien, The Fellowship of the Ring)

However, we shouldn’t despair though, there is some light going through the shadow!

  • Custom CSS Properties (CSS variables) bleed to the Shadow DOM.
  • ::part pseudo-selector

CSS Variables

The fact that CSS Variables not only bleed into the Shadow DOM, but are almost the only thing to do it, in my opinion, isn’t a limitation, it’s a great plus! And one of the main attractions for using a Web Components architecture.

This is especially true if you work on a UI Framework (and even more if your target audience doesn’t know and doesn’t care about CSS). This gives you complete control of the core styles in your component and extra protection for shady CSS rules and abusive usages of !important, and so on. You just need to make sure the styles in the Shadow DOM use the CSS Variables that you intend to expose to customization.

Of course, this will mean that your base :root will probably have a lot more variables defined, depending on your customization needs. When before we generally would only define the Design System’s foundations, like color, spacing and typography, now it could be useful to declare component specific variables, that punch a hole on the Shadow DOM to customize a specific element on the UI.

As with everything, there’s a balance that should guide those decisions. I’m not defending 1400 declarations (and definitely not with those naming conventions), but I wouldn’t be shocked with a :root containing a third of that. Obviously, the number isn’t what matters, as long as it makes sense with the chosen architecture. As an example, the Ionic Framework is now based on Web Components, and just for the button they have over 20 custom properties! That might sound like too much for you, but it probably makes sense for their needs and they have the architecture to support it.

On the flip side, please remember, the core functional styles are on the component’s Shadow DOM. Most of the CSS file would be composed only of the custom properties defined in the :root, and overall styles, like resets and layout.

CSS ::part selector

This is a great new selector, that allows you to specify a “styleable” part on any element in your shadow tree. By exposing “parts” of an element we can provide some flexibility in how a web component is used while exercising protection in other areas.

Here’s an example for opening up a section’s title to customization:

And using the usual pseudo-elements syntax to change the border-bottom. Please note that this can be done on your CSS file, outside of the Shadow DOM.

You can look at this great article from Monica Dinculescu, for further examples on this, as well as the differences between ::part and another promising selector — ::theme!

Looking forward

Heliocentrism

“Heliocentrism is the astronomical model in which the Earth and planets revolve around the Sun at the center of the Solar System.”

Copernicus Heliocentrism model.

For all we have been discussing, Web Components, in my opinion, are fitted perfectly to a more centralized approach. Imagine it just like a Planetary System, with the star at the center, while all other elements are living around, dependent on the core’s energy and gravity.

The Core

At this core, you would have a single file (JS or HTML) with all your Web Components, with all necessary functional CSS, HTML and Javascript. You can even have it on GitHub for broader availability, and with that someone can easily create their own branches to change something, like adding a custom event listener to a particular pattern, or adding a ::part selector to open some element for customization.

Of course, this is more aimed for anyone involved in building and maintaining a UI Framework, but it’s equally valid for an internal Components Library in your company. The main point is that it should be totally independent, from an architecture point of view.

The Orbits

If you haven’t read this great article by EightShapes, about Design Systems Tiers, please do so! In short, it details a more layered and tier oriented architecture for product specific Design Systems.

With Web Components, this architecture would scale well with any number of layers you need to add. In fact, it would make perfect sense to have a Tier 1 Design System to complement the core Web Components layer. On additional tiers, you could have the Tier 2 Design Systems, focused on particular product areas, like a Native Mobile App or a B2E Web Responsive App.

Remember, these could be feeding products using different technologies! Some in React, others in Angular, and even others in a low-code platform, like OutSystems. The core Web Components would work in any of them.

This might sound like a brutal change, but remember — just like html tags are almost 30 years old and independent of any Design Systems you build today or in the future, so could be your Web Components!

If you really needed to customize some components for a specific Tier or product area, you could easily create a new set of Web Components that would extend the original custom elements (or just create new ones).

The following image can help visualizing this idea:

Inflationary Epoch

“In physical cosmology the inflationary epoch was the period in the evolution of the early universe when, according to inflation theory, the universe underwent an extremely rapid exponential expansion.“

What are those fancy and pretentious words on the title, you may ask? Well, they’re pretentious, but according to its definition, I believe they’re a perfect representation of the current status of Web Components development (also, sounds cool…).

The Inflationary Epoch was the early period where the universe started to exponentially expand. I believe in the following years, Web Components will see a similar growth, and I honestly hope so!

Is it there yet? No. But browser support is getting increasingly more stable, and its adoptance is more or less inevitable, due to its core value and the fact that major players are contributing to its development.

In fact, one of the more promising features is a proposal from Apple — HTML Template Instantiation — where the main point (as far as I understand, which I don’t fully) is to add the main missing feature on the HTML Templates — instant refresh on data updates. Using the words of Google’s developers on a previous Google I/O event, this would bring:

  • Fastest way to create DOM
  • Fastest way to update DOM
  • Batched updates
  • Flexible primitives
  • Future declarative syntax

So, as you can see, there’s a lot of potential on all of these, and now is the time to start experimenting with it! It’s the opportunity to make our patterns native, more performant, more accessible and independent of whatever technology you want to wrap around them.

Like it or not, you will eventually have to make Web Components!

References

--

--

Bernardo Cardoso

Is an avid lifelong learner, and front-end developer at OutSystems who loves the collaborative, and problem-solving nature of his job.