Alligator.io
So your interested in learning about Web Components and creating your own custom HTML tags? In this post we’ll explore the basic syntax and concepts to allow you to start dabbling with Custom Elements and Shadow DOM.
We’ll create a silly <my-title> custom element that simply stamps out a styled title. Not very useful, but it’ll help demonstrate a few starting concepts.
First we’ll create a separate JavaScript file that will contain everything about our custom element: its style rules, its markup, the ES6 class definition and finally register the custom element. In HTML files where we want to use our custom element, all we’ll have to do is to include that JavaScript file and we’ll be good to go to start using the new tag on our page.
Let’s wrap everything in an IIFE for good measure:
(function() {
// the good stuff goes here
})();
Now let’s define a class for our custom element, which should extend HTMLElement:
(function() {
class MyTitle extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<style>
h1 {
font-size: 2.5rem;
color: hotpink;
font-family: monospace;
text-align: center;
text-decoration: pink solid underline;
text-decoration-skip: ink;
}
</style>
<h1>Hello Alligator!</h1>
`;
}
}
window.customElements.define('my-title', MyTitle);
})();
Here are a few things to note:
With such a simple custom element, you could also define the class as an anonymous class directly in the call to customElements.define:
(function() {
window.customElements.define(
'my-title',
class extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<style>
h1 {
font-size: 2.5rem;
color: hotpink;
font-family: monospace;
text-align: center;
text-decoration: pink solid underline;
text-decoration-skip: ink;
}
</style>
<h1>Hello Alligator!</h1>
`;
}
}
);
})();
You’ll notice that everything is contained within the JavaScript and that we don’t have any standalone HTML markup. That’s because, perhaps unfortunately, HTML imports seem to be dead in the water, and the way forward for Web Components will be to define markup and styles in the JavaScript using ES6 string literals.
The above example is all well and good, but there’s one major problem: our styles are not scoped to our custom element. That means that now all h1 tags on our pages will be hotpink with an underline. For this component’s styles not to impact its outer world, a ad hoc solution would be to wrap our markup inside something like a div and then apply styles with a selector that targets only our wrapper:
this.innerHTML = `
<style>
.wrap-my-title h1 {
font-size: 2.5rem;
color: hotpink;
font-family: monospace;
text-align: center;
text-decoration: pink solid underline;
text-decoration-skip: ink;
}
</style>
<div class="wrap-my-tile">
<h1>Hello Alligator!</h1>
</div>
`;
That’s not great and is not even foolproof if there’s another element with the wrap-my-title somewhere in your markup. Plus, it would be more performant and much nicer if we could use simple CSS selectors like h1. This is where Shadow DOM comes in. Shadow DOM allows us to scope our styles to our custom elements so that they don’t bleed out.
To use Shadow DOM, you attach a shadow root to the element and then define the markup for the element inside the shadow root:
(function() {
class MyTitle extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
h1 {
font-size: 2.5rem;
color: hotpink;
font-family: monospace;
text-align: center;
text-decoration: pink solid underline;
text-decoration-skip: ink;
}
</style>
<h1>Hello Alligator!</h1>
`;
}
}
window.customElements.define('my-title', MyTitle);
})();
Here are a few things to note:
Using Shadow DOM, here’s what the markup will look like in your browser’s console:
You could also accomplish the same result by creating a template element, setting its innerHTML and then cloning the content of the template as a new child to our shadow root:
(function() {
const template = document.createElement('template');
template.innerHTML = `
<style>
h1 {
font-size: 2.5rem;
color: hotpink;
font-family: monospace;
text-align: center;
text-decoration: pink solid underline;
text-decoration-skip: ink;
}
</style>
<h1>Hello Alligator!</h1>
`;
class MyTitle extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
}
window.customElements.define('my-title', MyTitle);
})();
Using our custom element is as simple as adding the script file to the page and then using our element as we would any other regular HTML element. Note however that custom elements should always have a closing tag:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="./my-counter.js"></script>
</head>
<body>
<my-title></my-title>
</body>
</html>
Keep in mind that our element is not quite production ready. As it is now, the element will only work in a few modern browsers. You’ll want to run the code through a transpiler like Babel for JavaScript features that are not supported across the board like ES6 classes or string literals and you’ll want to use polyfills for custom elements and shadow DOM. We’ll go over using Web Components polyfills in a separate post.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!