Web components are the future.

Web components logo

What if I told you that you can work with components without a framework. You don't need React. Not Angular, or Vue. You can make components now, and you don't have to install anything for them to work.

What are web components?

Web components are exactly that - component-based elements running right in your browser. Let's not go into details too much, instead, we'll just go right ahead and make a custom web component.

Shadow DOM

To be honest, I think this name sounds pretty badass. Sounds like the ninja version of regular DOM. But what is it? Well, actually, saying it's the ninja version of regular DOM is not far off. It's a piece of hidden DOM, unaffected by anything outside the shadows. Basically, it allows for scoped HTML (and, as a result, scoped CSS). The element under which we find the shadow tree (the bits that are hidden) is called the shadow root. That's all the terminology you need to know, and the actual usage will be clearer with an example.

A simple example

Let's do something simple. Let's make a component welcome-message that greets us. We would like to use it like so (let's say we're welcoming Stan):

<welcome-message informal>Stan</welcome-message>

And we want this to output the text "Hi Stan, welcome!", but if the element doesn't have the informal attribute, we want it to say "Good day, Stan.". We also want the text to be bold and underlined. First, we need to create a class that describes the behaviour of our component. Of course, we want it to have the functionality of a regular HTML element, so we're going to extend it from the HTMLElement class. In the constructor, we're going to create the shadow DOM, and that's all we're going to do for now.

// create the template - this is going to be our shadow DOM
const template = document.createElement('template');
template.innerHTML = '<p>Hello <slot></slot>, welcome!</p>';

class WelcomeMessage extends HTMLElement {
	constructor(){
		super();

		// clone the template's contents
		const shadowDOM = template.content.cloneNode(true);

		// attach the shadow
		this.attachShadow({mode: 'open'});

		// now we insert the template
		this.shadowRoot.appendChild(shadowDOM);
	}
}

The slot element allows you to place elements from the source into the correct place in the shadow DOM. In this case, we're just having an unnamed slot and so everything we put into the welcome-message element will be automatically placed inside that slot. Alright, so we have that class. Now to link this class to the welcome-message element, we add

customElements.define('welcome-message', WelcomeMessage);

to the above. That's all we gotta do! Now we want to add some logic to handle the informal attribute. There's two things we need to do - one, we need to let the class know this is an attribute we'd like to keep track of, and two, we need to update the text when it changes. Here's the code for that (it's fairly self-explanatory):

const template = document.createElement('template');
template.innerHTML = '<p>Hello <slot></slot>, welcome!</p>';

class WelcomeMessage extends HTMLElement {
	constructor(){
		super();
		const shadowDOM = template.content.cloneNode(true);
		this.attachShadow({mode: 'open'});
		this.shadowRoot.appendChild(shadowDOM);
	}

	// this should return an array with the attributes being watched
	static get observedAttributes(){ return ['informal']; }

	// this callback is going to fire anytime a watched attribute changes
	attributeChangedCallback(name, oldValue, newValue){
		// we don't need to check if the name is "informal" because
		// that's the only attribute we're watching anyway
		this.shadowRoot.querySelector('p').innerHTML = newValue === null
			? 'Good day, <slot></slot>.'
			: 'Hi <slot></slot>, welcome';
	}
}

customElements.define('welcome-message', WelcomeMessage);

That's the functionality. Now for the styling - we can simply put a style element with our styles inside the shadow DOM and that means it will be scoped to the shadow tree. In other words, we can change the template to

template.innerHTML = `
	<style>
		p {
			font-weight: 900;
			text-decoration: underline;
		}
	</style>
	<p>Hello <slot></slot>, welcome!</p>
`;

This won't bleed through to other p elements on the page, and styles from the main page won't affect the shadow DOM. Pretty nifty, huh?

But... why?

Okay, okay. Frameworks are a lot more polished. That's true. But also, frameworks output some horrible code sometimes. It's relatively big unreadable files, while web components serve the plain, gorgeous components right into the browser. This is not a very good argument, because it's mostly up to preference, so let me hit you with a better one. Let me ask you - what does your page load, before your scripts are being run? If I only download your page, and don't care about the styles or scripts or any other assets, what do I get? If you hand-wrote the document, presumably the document's outline is clear and makes a lot of sense. However, in frameworks, what people sometimes like to do, is have this:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<script src="do-everything.js"></script>
	<title>Made by {{ framework }}</title>
</head>
<body>
	<app-root></app-root>
</body>
</html>

the content will dynamically load in, using the app-root element to put everything in. As we all know, this works fine for the user, but from a semantics standpoint, this is a horrible practice. This document literally has no content, even though that's what the document is inherently supposed to do. Let's see from the other side though - what if your app is an image gallery. It holds a few images and you're making the page all fancy with it sliding from the right or left, being able to change the view, etcetera. I'll bet you - that's going to be a lot of divs because you need the layout to be flexible and you want to manipulate it with javascript.

Now here's the beauty of web components: instead of having an app-root that just loads your entire image gallery (including the images themselves), or a complex app that holds the content, but in an obscure way, we can simply write this:

<image-gallery>
	<img src="./cute-cat.png" alt="Cute cat drinking milk">
	<img src="./grumpy-cat.png" alt="A cat looking really grumpy">
	<img src="./surgery-cat.png" alt="Cat with one paw without fur">
</image-gallery>

This holds all the content, nothing more, is readable, and the functionality will be loaded in through out image-gallery component. As a bonus, we can use this component literally anywhere, because as mentioned before, the styles from the page itself don't bleed through. Essentially, this is the cleanest way to write this image viewer in HTML.

Conclusion

There's one thing I didn't mention. Browser support. Well, it's not bad! The major browsers all support it. No IE11, so if you want to support that, don't use this. Here's the caniuse page on it, and as a sidenote, don't mind about customized built-in elements (which allows your to customize already existing elements rather than creating your own). So for web components, the support is already more than 94%. Pretty good!

Bonus content!

Hopefully I've gotten you all excited about web components, so as a first treat, I'll give you a simple snippet that implements the previously mentioned image-gallery component. You can find it on gist.github.com, give it a try, play with it, do anything you want! Hopefully it sparks even more love for web components ^-^