Building Your Own Web Component Library
It’s time you learn how web components work by building them yourself
Web Components are not dead, in fact, they’re on the rise, and it would be great for you, as a web developer, to catch up!
There are plenty of libraries out there that give you a set of WebComponents ready to use, but if you’re learning about them, it’s always interesting to understand how they work internally before you dive into importing 3rd party dependencies into your project.
So in this article, I’m going to show you how you can create a set of “homemade” Web Components, just like grandma used to make them and use them to create a simple web page.
The list of components
To set the expectations right, here is the list of WC (Web Components) that I’m going to be building as part of this example. Don’t worry though, I’ll share the repo in case you want to extend it and keep going.
- Card: The classic
div
element with some border and optional shadow. This component will be the main container of everything we do. - Stack: I loved the idea of using a stack when I tested the Material UI component library, so I’m stealing it. This component will let you create either a vertical or horizontal stack of components.
- Button: Because what kind of library would it be if we can’t create a custom button? Exactly. This component will also give us a glimpse into the world of dealing with events inside a Web Component.
- Text Input: Also, pretty basic, but you get the idea, we’ll include the ability to add a label and optionally set it as a password field.
And that’s it.
Yes, it’s a short list, but we’re dealing with some interesting challenges here:
- Layout components like Card and Stack have to deal with nested components and we’ll make use of slots for that.
- And the Button will show you how to deal with events.
Let’s get going!
Building out the basics
Like with everything we do in life, it’s important to understand the basics before we do anything complex, and that is why I’m building this library.
In this article I’m not going to go into a lot of details about what WC are and how they work, I’m assuming you’ve done your homework. If you haven’t, I would encourage you to open a new tab, and go to MDN’s documentation about them.
That’s a great place to start, once you’re done reading for a bit, come back here.
That said, let’s quickly talk about WC.
The way I see them, is as a way for you to build your own, custom HTML elements that behave and look exactly as you need them to. And most importantly, that can be reused everywhere you go.
That last part is the key to WC, reusability. They’re slowly becoming the new web standard which means every single browser will understand them and know how to deal with them. Whatever you build will work, out-of-the-box, everywhere.
And that’s powerful.
The build
Here is a rough sketch of what I’ll be building:
It’s not that complicated, to create a WC, one of the things you have to do, is to extend the HTMLElement
class, and since all of our components will have the same lifecycle methods and some other common logic, we’ll export all of it into a generic CustomComponent
class, and we’ll have our “Homemade Components” extend that common class.
Note: The coloring of the last line is simply to indicate interactable components vs layout components, that’s all.
Understanding the lifecycle methods
Web components, like every component on any of the common UI frameworks around, have a life and a set of lifecycle methods that get called during different moments of it:
connectedCallback
: This method gets called when the element is connected (appended) to a DOM-connected node. Essentially, when the element is visible.disconnectedCallback
: The opposite of the previous one, when the element gets removed from the DOM for some reason.adoptedCallback
: This one gets called when the element gets moved to another document.attributeChangedCallback
: This last callback gets executed when any of the attributes of your custom component changes.
For my implementation I’m using the first one and the last one, but mostly the first one. We need to understand when our components get added, and what to do then.
Understanding the shadow DOM
If you look at the rough UML diagram from above, you’ll notice the shadowRoot
property inside the CustomComponent
class. That’s the shadow DOM for our components.
The shadow DOM is essentially an off-tree DOM that we’ll use to build our custom element inside. We have the full DOM API at our disposal, but until we attach the root to the main page’s DOM, our component won’t be visible.
The other interesting benefit we get here, is that it provides scoped CSS by default. Any CSS that we define for our shadow DOM tree will be scoped, ala Vue or Svelte. This means it’s a lot easier to define classes that do not collide with other CSS stylesheets once you start importing your library into other systems.
This is huge if you’re looking to create a big system around your WC library.
Implementing the CustomComponent class
Let’s start looking at some code, shall we?
The CustomComponent
library is probably the most complex one, the rest are pretty simple.
What do we need this class to do? Simple:
- It needs to create the shadow DOM for our components.
- It needs to provide a way for custom components to provide their own styles.
- It needs to handle the
connectedCallback
andattributeChangedCallback
methods. - It needs to keep track if the component has already been rendered or not.
And that’s pretty much it. So let’s go step by step.
Creating the “shadow root” and getting things going
The constructor of this class will take care of some of these tasks, including, of course, the creation of the so-called “shadow root”.
Let’s look at the code and then we can analyze it:
The first thing we need to do is to call the super
function, which will make sure we call the HTMLElement
constructor first. Then we create our shadow root by calling the attachShadow
method. The mode
set to “open” is simply a way to keep the shadow DOM changeable from outside the class. This is the recommended way of doing it, so let’s not really think too much about it right now.
We then move on to create a span
element for the mainComp
property. This will be the main wrapper for all of our components. I chose a span
because visually it means nothing unless we style it. So this gives me an anchor point that unless I want to, will not affect the layout.
I then go on to create another element, this time a style
block. This will be my set of scoped styles. It will contain the content of the customStyles
property, which we’ll set from within the implemented components (more on that in a minute).
We then attach our new wrapper and style blocks inside the shadow root and the rest is just irrelevant right now.
Implementing the secondary entry point: connectedCallback
After the constructor gets called by the browser, the second point of contact with our custom component will be the connectedCallback
method.
It will be called by the browser when it’s finally ready to attach our component to the main DOM.
In our case, this method looks like this:
The first thing this method does is call two other methods:
setUpAccessors
is an internal method that will turn every HTML property into a property inside the_attributes
variable. So if our component has anis-password
attribute, we’ll be able to read it withthis._attributes.isPassword
.- The
display
method will render the HTML of our component and it will attach it to themainComp
wrapper. Effectively attaching it to the root of our shadow DOM.
Finally, it will run through all childNodes
and it will append them inside our component. This is to make sure that text nodes inside our components also get added.
The display
method looks like this:
Now, one thing that is important to remember is that if you’re dealing with nested components, the connectedCallback
method will be called several times in cascade. And since we have the ability to nest components here (thanks to the Card
and Stack
components) we need to remember that fact.
This is why I’m first checking if the component has already been attached to the root node with the isAttached
property. If it hasn’t, then I’ll update the styles from the customStyle
property, and then I’ll call the render
method, which should be implemented by each specific sub-class. Then I’ll set the isAttached
property to true
and finally I’ll append my newly rendered component into the shadow root.
The next time this method gets called, it will return without doing anything.
And that’s pretty much everything you need to know about this class. If you’d like to see the full implementation, you can check it out here.
Implementing our interactive components
Remember, with all the hard work done for us with the previous class, implementing a new Web Component should be as easy as extending the above class and implementing the render
method.
So let’s see what the Button
component looks like:
First let’s look at the constructor
:
- We’re calling the
super
function first, of course. This is a must, otherwise you’ll get an error. - Then I’m setting the
compName
property, which is simply debug information. - And finally I’m setting up the content of the
customStyle
property. This property, if you go back to theCustomComponent
implementation, is the one that will populate the insides of the scopedstyles
block we define for every component.
Then for the render
method:
- I’m creating a wrapper with a
div
this time. - And then I use the
document
API to create all the elements I need. - I’m setting the text content of the button through my custom accessors (see line 33).
- Then I’m taking the content of the
onClick
attribute, and if it’s a string (which should always be), I’m going toeval
it. Note that this is dangerous, the content of this string should be checked before I do this, otherwise someone could benefit from me automatically running their code. - Finally, I append my button into the wrapper and return the wrapper.
The last interesting bit about this file, is not on the class, but underneath it. For the browser to know this class is meant to represent a Web Component, you have to define the custom element and attach the class to it. Line 48 shows you exactly how to do that.
Also note that Web Components need to have all lowercase names and a dash in the middle. You can’t define their name with a single word, so keep that in mind.
That’s all there is to it. Here is how you’d use this component:
<hm-button data-text="My Button" onClick="() => alert('Hi there!')"></hm-button>
Did you like what you read? Consider subscribing to my FREE newsletter where I share my 2 decades’ worth of wisdom in the IT industry with everyone. Join “The Rambling of an old developer” !
Implementing layout components
The layout components, or essentially, components that let you nest other components inside are slightly different.
And I say “slightly” because they need to have a slot
element inside them, which will tell the browser where to put what you add into them.
Here, take a look at the Stack
component:
The constructor
is the same as before, we set the styles. In our case, we’re using flex-box to determine the direction of the stack, but that’s it.
Then for the render
method, instead of using the document
API for everything, I’m setting the wrapper’s innerHTML
property with a template string, and there I’m defining the slot
element.
This element will indicate where things go when the browser starts reading custom components being nested. Trust me, if you don’t add it, you’ll have some very strange rendering results.
The rest of this method is simple logic to decide which CSS classes to use.
Testing things out
You’re more than welcome to check out the repo and review the code, you’ll find a few more components and methods there that I didn’t cover here because they’re not that relevant.
But if you’re wondering what the HTML of something using these components looks like, here is my test page:
Note that there is no JavaScript whatsoever. The rendered result looks like this:
I know it’s not pretty, but you get all the elements there.
Finally, here is what the rendered HTML looks like using Chrome’s Dev tools.
This is the most interesting screenshot, because you can see both, the actual Web Component element (i.e hm-card
and so on) and inside them, you can see the shadow root nodes which contain the actual HTML we created for them (the style
and span
wrapper can be seen there).
This makes it simple to debug and understand if you’re having problems getting things to look right.
Web Components are not a new technology, they’ve been evolving for more than a decade, and they’re now ready to become the new standard. So make sure you understand how they work. They’re compatible with all major frameworks if you know how to use them, so give them a try.
If you have any questions about this code, leave a comment and I’ll do my best to help!
See you on the next one!
Build apps with reusable components like Lego
Bit’s open-source tool help 250,000+ devs to build apps with components.
Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.
Split apps into components to make app development easier, and enjoy the best experience for the workflows you want: