Why Stencil.js might just be the best way for near-vanilla web components
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
- 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 theattachShadow()
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:

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

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";
@Component({
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. usingaddEventListener
)
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:

(https://twitter.com/jthoms1/status/1176941324123201542)
References
(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.