Why Stencil.js might just be the best way for near-vanilla web components

by Emil — 7 minutes

Although frameworks like React, Vue.js and Angular are really great, UI components created in these frameworks are not reusable in an other framework.
Also using a framework will often bloat the bundle size of your UI component.

Instead we could use solutions like LitElement or Stencil.js to create near-vanilla Web Components, which can just be used in all the frameworks.
And since solutions like LitElement or Stencil.js introduce almost no additional overhead compared to Web Components written in Vanilla JavaScript, I personally like to use the term near-vanilla web components for them.

Especially because all major browsers, except MS Edge, now offer native support for the core standards of Web Components being Custom Elements v1, Shadow DOM v1 and HTML Templates.

Why Shadow DOM is problematic

Even though browsers now have native support for Web Components, usage of Shadow DOM is problematic:

  • Since v1 it’s no longer possible to use CSS selectors ::shadow and /deep/ to pierce the Shadow DOM

  • And alternatives like CSS Shadow Parts (::part) and :host-context are currently only supported by Chrome; also the Safari team refuses to implement :host-context and even requested its removal from the specification

  • Also Shadow DOM negatively affects accessibility as described by Rob Dodson

  • Form autofill for form elements in a Shadow DOM is broken in both Chrome and Safari

  • Also handling of focus is affected by Shadow DOM, especially the navigation order when tabbing to a focusable element like the <input> element.
    To fix this the “delegatesFocus” can be passed to the attachShadow() invocation but for now only Chrome supports this

  • Safari does not return nodes from the Shadow DOM from document.getSelection()

  • Currently a shadow root can "not" be defined declarative in HTML, causing issues for Server-Side Rendering (SSR) Probably, this won’t be fixed any time soon since the "Declarative Shadow DOM" proposal was rejected

Safari specific Shadow DOM issues

Although Safari officially fully supports Shadow DOM v1, it turns out that there are still bugs in their implementation according to their WebKit Feature Status website:

Screenshot of the webkit feature 'Shadow DOM' spec part of Web Componentgs, displaying that it is supported

To investigate I also looked at caniuse.com that shows the following information:

Screenshot of canIuse stating that Safari & Chrome for iOS 13.0-13.1 have partial support for Shadow DOM

As it turns out, looking at the WebKit issue "Implement v1 shadow DOM API" (see "Depends on" section), the Safari implementation of Shadow DOM v1 still contains a lot a bugs; of which many already seem to exist for quite a while.

Alternatives for Shadow DOM

As with any of the core Web Components standards, using Shadow DOM is not required when creating a Custom Element.

But by not using Shadow DOM you are missing out on two major features of Shadow DOM:

  • CSS isolation

  • Slots

As it turns out LitElement does not support a fallback for Shadow DOM, except by forcing the usage of the (slow) Shadow DOM polyfill.

So instead I started to investigate Stencil.js that does offer an alternative to Shadow DOM, an emulated scoped CSS and emulated slots implementation.

Introducing Stencil.js

By using Stencil.js (likewise with LitElement) you can create near-vanilla Web Components without the overhead of a framework.

However Stencil.js takes a radical different approach than the LitElement library. Stencil compiles your code into a Custom Element that optionally uses Shadow DOM.

Looking at the code of Stencil components it looks like a hybrid of Angular and React:

  • it uses TypeScript including Angular-like annotations @Component and @Prop

  • just like React it uses TSX to embed markup in your TypeScript code

The example below illustrates what a Stencil component looks like:

import { Component, Prop, h } from "@stencil/core";

  tag: "my-embedded-component",
export class MyEmbeddedComponent {
  @Prop() color: string = "blue";

  render() {
    return <div>My favorite color is {this.color}</div>;

Since the release of 1.0 in June 2019, Stencil.js appears to become more and more popular. Hence Apple is now even using Stencil.js in their new beta of the Apple Music Web Client.

Using emulated CSS scoping in Stencil.js

By specifying either shadow: true or scoped: true to the @Component decorator of a Stencil component, you can opt-in by using respectively the Shadow DOM or an emulated scoped CSS.

By using scoped: true you will get CSS scoping similar to the (default) emulated view encapsulation in Angular and <style scoped> in Vue.js.

Just like other emulated scoped CSS solutions from Angular and Vue, you will probably not be missing the full CSS isolation from the Shadow DOM.
The scoped: true will protect you against styling leaking out of your component, but (similar to Angular and Vue.js) it will not protect you against (global) styling leaking into your component. But leaking in styling should not really be an issue when certain safe-guards are in place like CSS namespacing.

Besides scoped CSS, usage of scoped: true also emulates the Shadow DOM v1 specific CSS selectors :host, ::slotted and even :host-context (that Safari refuses to implement)

Stencil.js framework bindings will be open-sourced

The best feature of Web Components, or more specifically Custom Elements v1, is that web components can be used inside any framework through HTML.

However standard HTML is somewhat limited when it comes to using a custom element:

  • as standard HTML only supports specifying HTML attributes that have a string value

  • as standard HTML is "not" case-sensitive

  • as standard HTML lacks support for handling a CustomEvent

These limitations can cause issues when using web components because:

  • unless a framework adds support for property binding, you can only specify string values via HTML attributes

  • unless a framework has build their own HTML parser (like Angular 2+), names of (custom) events specified in HTML will "not" be case-sensitive.

  • unless a framework adds support for handling a CustomEvent from HTML, you will have to resort to JavaScript to register an event listener (e.g. using addEventListener)

Luckily most frameworks support doing property binding on a custom element. And also most frameworks support handling custom events from HTML, and some even support case-sensitive event names.

However the React support for custom elements is rather miserable since:

  • React does "not" support property binding in HTML for custom elements

  • React does "not" support handling a custom event through HTML

To be able to integrate a custom element into React, you will need a React component to act as a bridge between React and the custom element. Fortunately you can use a libraries like React Custom Element Wrapper for this, but then you will still have to manually specify a mapping for each attribute / property / event of the custom element to a React prop.

More detailed information about React and support of other frameworks for custom elements can be found on the website Custom Elements Everywhere

To offer seamless framework integration of custom elements, Ionic (the company behind Stencil.js) offers a commercial solution called StencilDS that, amongst other things, offers a code generator for framework specific bindings.
And recently Ionic announced that they are planning to open-source this functionality, making Stencil.js by far the best solution IMHO for creating near-vanilla web components:

Screenshot of a tweet by Josh Thomas stating 'We have decided to open source framework bindings for StencilJS.  This means that you will get the ability to generate React and Angular libraries to use in your applications. More info soon.'


(In dutch) In-depth sessie over Near-Vanilla Web Components

Op woensdag 27 november organiseren wij om 18.00 uur op ons kantoor in Nieuwegein een in-depth sessie over Near-Vanilla Web Components.

Tijdens deze in-depth sessie willen we near-vanilla web components neerzetten als serieus alternatief door in te gaan op achterliggende (toekomstige) web standaarden.

Ook zal tijdens de sessie worden ingegaan op de praktische problemen bij het gebruik van web components binnen een framework zoals React.

Voor een hapje en een drankje wordt uiteraard gezorgd. De sessie zal rond 21.00 uur eindigen.

Een meer uitgebreide beschrijving over de in-depth sessie van 27 november is hier te vinden.