This blog post covers everything you need to know about web components, its libraries, and how we leverage it at Dyte.
Web components (or Custom Elements) is a set of standardised APIs that allow you
to create reusable and encapsulated components for the web. Custom elements
allow you to define your own HTML elements and their behaviour using JavaScript.
This means you can create your own custom elements like <my-element>
and
define their behaviour using JavaScript.
It has been around for quite some time now, but it hasn’t gained huge traction in the frontend world as most are still quite unfamiliar with it, although it’s being widely used in top websites such as YouTube, GitHub and many more.
There are many benefits to using Web Components, such as:
Before we dive deeper into web components, let’s understand the core terminologies or APIs.
Here is a minimal counter example from webcomponents.dev:
<script>
const template = document.createElement('template');
template.innerHTML = `
<style>
* {
font-size: 200%;
}
span {
width: 4rem;
display: inline-block;
text-align: center;
}
button {
width: 4rem;
height: 4rem;
border: none;
border-radius: 10px;
background-color: seagreen;
color: white;
}
</style>
<button id="dec">-</button>
<span id="count"></span>
<button id="inc">+</button>`;
class MyCounter extends HTMLElement {
constructor() {
super();
this.count = 0;
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.appendChild(template.content.cloneNode(true));
this.shadowRoot.getElementById('inc').onclick = () => this.inc();
this.shadowRoot.getElementById('dec').onclick = () => this.dec();
this.update(this.count);
}
inc() {
this.update(++this.count);
}
dec() {
this.update(--this.count);
}
update(count) {
this.shadowRoot.getElementById('count').innerHTML = count;
}
}
customElements.define('my-counter', MyCounter);
</script>
<my-counter></my-counter>
Let’s look at the parts of the above code snippet in detail to further understand web component a bit better.
First, we’ve written the actual content inside a <template>
tag. This element
allows you to write markup template which isn’t actually rendered on the page,
so that the contents can be re-used anywhere in the page. There is also a
similar tag: <slot>
which is a placeholder inside a component where you can
drop in your own content, kind of like how you pass children
in React and
slots
in Vue, but the difference being that it creates separate DOM trees and
presents them together.
Next, we define our custom element by extending HTMLElement
and we initialise
the component inside the constructor()
. We also attach a Shadow DOM to the
component in the constructor: this.attachShadow({ mode: 'open' })
in the
open
mode, which means the elements inside this Shadow DOM are accessible from
outside the shadow root. Whereas in a closed
mode, access to elements is
denied from outside the shadow root. You access the shadow root with
this.shadowRoot
.
And like other frameworks (take React, for instance) Web Components also have
it’s life cycle callbacks like: connectedCallback()
, disconnectedCallback()
,
attributeChangedCallback()
and adoptedCallback()
. Here’s a brief explanation
of each of these:
connectedCallback()
- Called when the component is first connected to the
DOM.attributeChangedCallback(name, oldVal, newVal)
- Called when an attribute to
the component changes.adoptedCallback()
- Called when the component is moved to a new document.disconnectedCallback()
- Called when the component is disconnected from the
DOM.In our example, we initialise the DOM with the content we defined in the
template, and we attach event listeners to our DOM nodes which mutate the state
on user interaction, and then finally we initialise the state in the DOM with
this.update()
.
Finally we register the created element in the
[CustomElementRegistry](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry)
with a specific tag name, so we can start using it in the DOM.
customElements.define('my-counter', MyCounter);
Note that custom elements tag names should be at-least two words separated by hyphens, so that they don’t conflict with any new HTML elements that are created in the future.
Though, there are some limitations of Web Components too:
You can easily overcome these limitations with the help of a web component library.
While you can write web components without the need for a third party library, there are some libraries out there which help us to write Web components much more easily and provide other features and performance improvements on top of the standard APIs, such as StencilJS, Lit, FAST to name a few.
Here’s a quick walkthrough through the above mentioned libraries to learn their differences:
What’s common among all these libraries is that they allow you to write in TypeScript and provide similar APIs.
We did experiment with a wide range of libraries, and decided to use Stencil for the following reasons:
Unlike other libraries, Stencil components don’t extend HTMLElement
like we
saw in the counter example. The reason which Stencil states is:
HTMLElement
locks source code to only work in the browser, rather than keeping the logic free from any APIs. The Stencil compiler is able to optimize many versions of components for various output targets, and one way to make that easier is by keepingHTMLElement
out of it.
While web components theoretically should work anywhere, there isn’t full support for leveraging it in its entirety in some frameworks like React and Angular, while it does work really well in recent frameworks like SolidJS, Svelte & Vue which come with better Custom Elements support.
However, React is bringing full web component support (passing props and handling events), and it’s currently in the experimental stage, so until it fully reaches stable, we’re gonna have to find a way to use our components in React Apps!
You can check out Custom Elements support in various frameworks here: https://custom-elements-everywhere.com/#react.
Luckily Stencil provides easy ways to create component wrappers for frameworks like React and Angular. This nice guide helped us to create a React UI Kit package, which adds wrappers over our web components and exports React components. They have similar guides for Angular as well.
I had created a Stencil monorepo starter which has all the things you need to start shipping UI Kits for Vue, React and Angular which are written in Stencil, you can check it out here: https://github.com/vaibhavshn/stencil-monorepo-starter.
We have an example component
[my-component](https://github.com/vaibhavshn/stencil-monorepo-starter/blob/main/packages/core/src/components/my-component/my-component.tsx)
in the Stencil core package, which can be found at packages/core
.
When we build the core
package, you’ll see that a React component
MyComponent
is auto generated in this file:
https://github.com/vaibhavshn/stencil-monorepo-starter/blob/main/packages/react-library/src/components/stencil-generated/index.ts.
Similarly Angular and Vue components are also generated in their respective
paths.
Finally, we can build the individual react, angular and vue libraries and ship them.
Regarding styling our UI Kit components, we use a custom design system so our customers get the fullest customisation options possible, and we use TailwindCSS to have a single source of truth for our design tokens which we can use anywhere across our components, which made our CSS highly maintainable and configurable.
Setting it up is pretty straight forward as Stencil supports postcss via a plugin.
Here’s how you can set it up.
First, install the packages:
npm i @stencil-community/postcss tailwindcss autoprefixer
Then, to create tailwind’s config file, run:
npx tailwindcss init
Now, we need to configure Stencil to use TailwindCSS via the postcss plugin in
stencil.config.ts
:
export const config: Config = {
// ...
plugins: [
postcss({
plugins: [require('tailwindcss'), require('autoprefixer')],
}),
],
};
Since each Stencil component has it’s own css file, you’ll need to use it
@apply
directive or the theme()
utility in the css files instead of using
utility classes in your markup, like this:
:host {
color: theme(colors.gray.800);
/* or */
@apply text-gray-800;
}
Now you can define your design tokens in a central tailwind.config.js
file and
use it in every component easily.
There were however some challenges that came along with using Stencil.
We noticed there was a memory leak related to DOM elements happening, and after debugging found out that Stencil wasn’t cleaning up detached DOM nodes properly. We’ve notified the team about it and it is being looked at.
Also Stencil components are lazy loaded for performance. However there is a
drawback to it. As our application has a lot of interactive components, whenever
you render a component for the first time, it doesn’t render ASAP, rather we
have to wait for the component code to load over the network (using dynamic
imports) and then it will render. There is a slight lag. Stencil had this with
their dist-custom-elements-bundle
output but they’ve deprecated that in favour
of the dist-custom-elements
output. We have found a way around this where we
inline dynamic imports and now our components are loaded immediately and render
fast too.
Another cool thing about Stencil is that it comes with support for generating documentation from JSDoc and other documentation files.
Here’s how you would normally document a component with Stencil.
import { Component, Prop, h } from '@stencil/core';
/*
* A component which tells you your name
*/
@Component({
tag: 'my-first-component',
})
export class MyFirstComponent {
/** Name of the user */
@Prop() name: string;
render() {
return <p>My name is {this.name}</p>;
}
}
Like this you can write documentation for a component, its properties, events and methods.
Once you’ve written the documentation, you can generate the documentation in
various formats with the various docs output targets that Stencil provides. You
can choose among json
, readme
or custom
outputs.
We chose the json
format because we wanted more control over how to showcase
our documentation in our documentation site.
Here’s the documentation page for the DyteSimpleGrid
React component. You can
see it here:
https://docs.dyte.io/react-ui-kit/components/dyte-simple-grid
The above image showcases how we show documentation for our DyteSimpleGrid
component.
Also note that the examples for each UI Kit - React UI Kit, Angular UI Kit are
different. We wrote framework-specific usage examples in the files:
usage/react-example.md
, usage/html-example.md
.
Then Stencil reads these files and outputs them in the docs output file, and we use that to generate framework-specific documentation in our docs site.
We wrote a custom script for our docs which processes data from the Stencil output and generates documentation for HTML, React and Angular UI Kits!
Also, we show Live Previews for our React UI Kit documentation so users can play around with our components right in the documentation!
Now, all we have to do to update our documentation is to just write it along with the source code, and then just install the updated ui-kit package in our docs and our documentation auto-updates! 🎉
So to conclude, most of our customers are currently using our UI Kit, and we’ve got some great responses from them about how easy it is to use and configure.
However, we’re still trying to push the boundaries and are exploring new ways to bring more customisation to our offerings, so stay tuned!
If you haven’t heard of Dyte yet, go to https://dyte.io to learn how our SDKs and libraries revolutionize the live video and voice-calling experience. Don’t just take our word for it; try it for yourself! Dyte offers free 10,000 minutes every month to get you started quickly. If you have any questions or simply want to chat with us, please contact us through support or visit our developer community forum. Looking forward to it!