Custom Elements and the Sites of Yore

Simon Attfield
4 min readAug 24, 2023

tl;dr — This article is intended to be a very basic intro to Custom Elements. If you’re already familiar with them then, fair warning, you may be bored. But if you’re looking to get started or see how you could implement them, please read on.

Recently I got introduced to an old codebase, so old in fact that no one could tell me how old it actually is (there’s articles dated back to the late 90s). However, it’s served the client well and over the years has grown to 1600+ pages of articles, blogs, and alerts. A veritable smorgasbord of information that would be sad to lose.

My task was to create two new pages and overhaul the remaining pages with some simple styling updates. The design for the new pages is elegant, bold, and modern, with a plethora of animated elements to bring the page to life. Normally a simple task, but with the impressively daunting codebase I had in front of me, I needed a way to integrate more modern elements simply and quickly. I didn’t have the time to learn the inner workings, and I didn’t want to add more libraries or new frameworks to the mix. So, could I do something basic while still encapsulating functionality and keeping things portable? Sure, I could have dusted off the old jQuery hat, but nowadays I find it uninspiring and there was already enough dotted around polluting the global scope. I really wanted something I could keep small and out of the way.

Custom Elements to the rescue!

Maybe it’s me, but I don’t think we talk about Web Components all that much. When I’ve talked to friends/colleagues, the assumption is we’re talking about the Shadow DOM and client side rendering. And sure, it does that. But it doesn’t have to. So I want to take a quick look (perhaps a quick intro for some of you) to one aspect of the Web Component spec, Custom Elements, and using them in a static page.

In its most basic terms, with a Custom Element you’re just extending an existing element with a couple of bells and whistles to suit your needs. And the way I’ve been using it lately feels akin to an AngularJS directive (for those of us old enough to remember when AngularJS felt like magic).

In order to demonstrate, let’s build something monumentally basic. The ol’faithful counter.

Here’s the HTML:

  <my-counter count="0">
<p>Current count: <span></span></p>
<button class="decrease">Decrease count</button>
<button class="increase">Increase count</button>
</my-counter>

I’m creating a <my-counter> element and providing an initial prop of count="0" . Nothing fancy, and that’s kinda the point. On it’s own it’s just bit of text and some buttons, but if JS fails or is turned off then you still get something.

Let’s hook this to something more interesting. We start by defining our element, and what existing element we want to inherit from:

customElements.define('my-counter', class extends HTMLElement {
constructor() {
super();
}
});

Here we’re just saying “whenever you see a <my-counter> run whatever is here as if you’re a basic HTMLElement . This could be more specific should you need it. Want to inherit all the extra goodies that come with an <li> ? Then just extend HTMLLiElement instead.

Now lets get our inner elements:

customElements.define('my-counter', class extends HTMLElement {  
constructor() {
super();
this.increaseBtn = this.querySelector('.increase');
this.decreaseBtn = this.querySelector('.decrease');
this.countSpan = this.querySelector('span');
}
});

Notice the this.querySelector , so we’re only looking inside our element and don’t need to query the whole DOM.

Now to set up some initial functionality, we can use the built in method connectedCallback() which runs whenever a <my-counter> is added to the DOM:

  connectedCallback() {
this.countSpan.textContent = this.getAttribute('count');

this.increaseBtn.addEventListener('click', () => {
this.setAttribute('count', Number(this.getAttribute('count')) + 1);
});

this.decreaseBtn.addEventListener('click', () => {
this.setAttribute('count', Number(this.getAttribute('count')) - 1);
});
}

Not my cleanest code, but for this example you’ll see that we’re changing the count attribute on our element. On its own, it does nothing. But takes surprisingly little to finish off our little example.

First, we tell the element which attributes to watch…

static get observedAttributes() { return ['count'] }

…then, we use another built in method that fires when it sees a change.

attributeChangedCallback(name, oldVal, newVal) {
this.countSpan.textContent = newVal;
}

And that’s it. Encapsulated, portable, lightweight, no framework, and still pulling in the monolithic, pre-existing, CSS.

Here’s the full code:

customElements.define('my-counter', class extends HTMLElement {
static get observedAttributes() { return ['count'] }

constructor() {
super();
this.increaseBtn = this.querySelector('.increase');
this.decreaseBtn = this.querySelector('.decrease');
this.countSpan = this.querySelector('span');
}

connectedCallback() {
this.countSpan.textContent = this.getAttribute('count');

this.increaseBtn.addEventListener('click', () => {
this.setAttribute('count', Number(this.getAttribute('count')) + 1);
});

this.decreaseBtn.addEventListener('click', () => {
this.setAttribute('count', Number(this.getAttribute('count')) - 1);
});
}

attributeChangedCallback(name, oldVal, newVal) {
this.countSpan.textContent = newVal;
}
});

Or see it working on CodePen:

https://codepen.io/attfields/pen/JjwdBQW

Yes it’s a ridiculously basic example…

…but using this basic idea I managed to build an entire animation library. Manipulating CSS custom properties and triggering intersection observers with surprisingly little code.

Are there better ways to do this? …maybe.

Did I do this to keep my brain from collapsing in on itself as I wade through code spaghetti and keep the spark alive? ….yeah, absolutely.

Am I only going to work like this in the future? Absolutely not. But for quick prototyping, now my head can be in this space, it’s invaluable.

As front end developers I feel it’s important to understand as many tools as we can. Know what is available to us so we can consciously decide on the right tool for the job. So, next time the discussion falls to Svelte, Apollo, React, Vue, or the cacophony of other tools out there, perhaps give a glance to the lowly Custom Element, or even Web Components as a whole. It might be all you need.

--

--