Building Your Own Web Component Library

It’s time you learn how web components work by building them yourself

Fernando Doglio
Bits and Pieces
Published in
11 min readOct 12, 2022

--

Photo by Roselyn Tirado on Unsplash

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 and attributeChangedCallback 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 an is-password attribute, we’ll be able to read it with this._attributes.isPassword .
  • The display method will render the HTML of our component and it will attach it to the mainComp 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 the CustomComponent implementation, is the one that will populate the insides of the scoped styles 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 to eval 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.

Learn more

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo

--

--

I write about technology, freelancing and more. Check out my FREE newsletter if you’re into Software Development: https://fernandodoglio.substack.com/