Garden-Variety Custom Elements

work in progress

Custom elements are simple, but powerful. Yet they are easily misunderstood these days, often because they’re mistaken for something they were never meant to be. Let’s just look at what it is they actually do – and don’t do.

As an early adopter of custom elements, I always understood them to be fairly straightforward: Back then, they mostly saved me the trouble of manually re-initializing components after the DOM was updated by some other part of the system (think tabs being injected into the page). Thus I was never tempted to confuse them with rendering libraries or even heavyweight frameworks.

However, because technology doesn’t exist in a vacuum, culture and momentum can play an important role in how such things are perceived. So it’s perfectly understandable that it took a while for the concepts to catch on as people worked through preconceived notions; depending on your background, the idea and benefits of custom elements might not be immediately obvious.

I recently heard a suggestion that we might need an educational playground like Flexbox Froggy. I’m not aware of anything like that, but it’s a compelling idea: I frequently find myself explaining custom elements and web components in an ad-hoc fashion, so I could really use a concise resource to point to. Unfortunately, I don’t have the wherewithal to even approximate that interactive tutorial’s style or quality (not least because this field is less deterministic) – however, I can at least try summarizing the fundamentals; a different kind of primer, if you will.

It’s Just HTML

At their core, custom elements are just HTML elements with funny names. Authors can make up an element name and use it in their documents without asking for permission:

<p>I talked to <cool-person>Les</cool-person> the other day.</p>

By convention, custom names should use hyphens, mostly to avoid conflicts with standardized elements (both current and future). Other than that, browsers and other HTML parsers basically don’t care; they tolerate unknown elements and treat them much like <span>s.

Of course this means there’s no inherent significance to custom elements: No styling, semantic value (e.g. for assistive technology like screen readers) or fancy behavior.

It’s Just CSS

Now that we know we can just invent elements, why not invent a use case too? Let’s say we want to write about our friends and decorate their names with emoji. After concluding there’s not a more suitable standard element in this case (though that’s always debatable), we might decide on something like this:

<p>
    Last night I met up with <cool-person>Kim</cool-person>,
    <cool-person>Nikhil</cool-person> and
    <cool-person>Ines</cool-person>.
</p>
cool-person {
    font-variant: small-caps;
}

cool-person::before {
    content: "👤 ";
}
Last night I met up with Kim, Nikhil and Ines.

Upon seeing this, our friends revolt: They’re not empty silhouettes! Let’s allow them to choose their own emoji:

<p>
    Last night I met up with
    <cool-person avatar="🧑‍🔬">Kim</cool-person>,
    <cool-person>Nikhil</cool-person> and
    <cool-person avatar="🥽">Ines</cool-person>.
</p>
cool-person::before {
    content: attr(avatar, "👤") " ";
}
Last night I met up with Kim, Nikhil and Ines.

So we can not just invent element names, we can do the same for attributes too! (Though we must take care to avoid redefining standard attributes like id or class.)

It’s Just JavaScript

That’s all well and good, but typically we’d employ custom elements to attach some custom functionality. Let’s do something a little more elaborate here: A simplistic CSV viewer.

In the spirit of progressive enhancement, we start with something that’s useful for everyone:

<csv-viewer>
Lipsum, Cicero, 45 BCE
Hello World, Kernighan, 1972
</csv-viewer>
csv-viewer:not(:defined) {
    white-space: pre;
}
Lipsum, Cicero, 45 BCE Hello World, Kernighan, 1972

:defined kicks in as soon as there’s a JavaScript definition for our custom element. Let’s add that:

class CSViewer extends HTMLElement {
    connectedCallback() {
        let rows = csv2rows(this.textContent.trim());
        this.replaceChildren();
        this.append(...rows);
    }
}

customElements.define("csv-viewer", CSViewer);
Lipsum, Cicero, 45 BCE Hello World, Kernighan, 1972

Henceforth, a <csv-viewer> element anywhere in our HTML will transform CSV data within that element into something more legibile.

NB:

There’s a subtle gotcha here: connectedCallback is invoked as soon as a corresponding element’s opening tag appears in our document. So if our custom element’s JavaScript definition has already been registered by the time such an element appears, our CSV content might not be there yet.

NB:

TODO:

Web Components