There’s been a recent flurry of articles about web components, with advice on how to shape them as extensions of HTML. I decided to dig in, and see how these ‘HTML web components’ could become a part of my own workflow. Despite a few rough edges, I’m excited to see where this feature is heading!
I’ve been intrigued
by web components
for some time,
from a safe distance.
I like things that are part of the web platform,
but mostly when that means
I don’t have to write much JavaScript.
It’s not that I can’t write JS,
or even that I don’t like to.
I enjoy procedural code!
And I’ve learned enough JS
to do what I need –
especially here in our
Eleventy setup.
Sometimes,
I’ll even write JS
that runs in the browser.
It’s fun,
and it’s useful,
but it’s not my default.
I’m a declarative programmer at heart.
I was raised by semantic HTML,
and I live by the rule of least power.
I don’t even flinch
when I see a declared value cascade,
resolve, and inherit.
But I get very cautious about
instructing the browser
to do things my way.
From a distance,
watching through well-organized venetian blinds,
that’s what I see.
Web Components are JavaScript files
that generate HTML&CSS along the way.
Often the HTML and CSS are written
as template literals,
or worse –
constructed in a long series of functions.
You might even call them
HTML-and-CSS in JS.
A few years ago,
web components still seemed
like a niche technology.
That’s been changing for some time,
but these days my Mastodon &RSS feeds
are full of web components.
And some of the posts
are coming from declarative nerds like me!
Just last week:
I recommend all of them.
And they’re not alone.
I’ve been hearing the Good News constantly,
from folks like
Cassondra Roberts,
Zach Leatherman,
Dave Rupert,
and Westbrook Johnson.
Now John Allsopp is
writing a book on web components.
I look forward to reading it.
I don’t know what changed.
Did a browser ship a thing?
Probably.
But what struck me about
last week in particular
was the declarative angle –
the pitch being made for
HTML-like web components.
Several of these articles
refer back to an earlier issue of
Robin Rendle’s excellent newsletter,
The Cascade –
and specifically the subject line:
What Would HTML Do?
That question,
and the flurry of posts expanding on it,
flipped a switch in my brain.
I’ve been trying to build web components
out of HTML,
but they are building web components
into HTML.
Every web component I’ve written
up until this point
started with a shadow DOM-defining template.
To me, that made sense as
the most declarative
place to start:
you start by writing declarative HTML:
<template><style>/* Normal old CSS */</style><section><slotname="title"><h2>Normal Old HTML, With Slots!</h2></slot><p>
This is
the most declarative component,
I say to myself
as I register it
using a JavaScript function.
</p></section></template>
It’s possible to do this in a way
that all your components auto-register
with minimal JavaScript
and no additional custom behavior.
For example,
we can loop over all templates
that have a data-register attribute,
and generate a custom element from each:
That works relatively well
if you only need a basic template
with markup and styles,
to achieve style ‘encapsulation’
or avoid repetition.
Then the templates can be written
in HTML,
with CSS in <style> tags as needed:
<!-- defining the component --><templatedata-register="media-object"><style>/* Media object styles */</style><divclass="object"><divpart="media"><slotname="media"></slot></div><divpart="content"><slot></slot></div></div></template>
<!-- using the component --><media-object><imgslot="media"src="…"alt="…"/><p>content …</p></media-object>
I’ve used HTML
templating languages
since the early 2000s –
from Django templates to Handlebars,
Twig, Jinja & Nunjucks,
Vue, Svelte, Liquid, etc.
It seems to me like these quick
web component templates
with shadow DOM
can generate a rough equivalent
of the most basic template language use-cases.
You get some amount of progressive enhancement
if you set it up right,
and you get CSS encapsulation for free,
even if you don’t love how that works.
But I also find it challenging
to get the slots and parts
set up correctly for styling.
That may just be inexperience.
And then I wonder,
progressive enhancement
compared to what exactly?
In my example above
all the content is accessible from first load,
without the custom element registration.
That’s better than some SPA frameworks,
but server-side templates
would get us even farther –
no run-time JS required.
And what if we want to pass in data,
then use flow control
to render our component?
That’s not a wild use-case.
Many sites and applications
need some way to
generate HTML from a database or CMS.
I suppose I could do it with more JS.
This is where tools like
WebC
can come into play,
providing a server-side build step
based on web component syntax,
with additional control-flow features:
<article><slotname="title"><h2>Normal Old HTML, With Slots!</h2></slot><time@html="formatDate(now)"></time><pwebc:if="showSummary">
This is still declarative,
I say to myself
while injecting JS
directly into its attributes.
</p></article>
WebC is great for other reasons.
It will pre-render whatever it can,
and only require browser JS
for things that need dynamic rendering.
But the further I go down this path
of data-to-html templating,
the heavier my components feel –
and the less declarative.
I end up writing mostly empty elements,
with all my content
hidden away inside private custom attributes.
The problem with this isn’t WebC,
or any of the other web component libraries.
You can easily get to that same place
without any build steps at all,
just by using shadow DOM
with more attributes than slots.
It must be a common issue,
because every article above
mentions the enticing danger
of ‘empty’ web components,
where all the content is provided through attributes.
They all blame React for this trend,
but I think that’s too simple.
I’ve been passing arguments
to component templates
since my first time using PHP
in the early 2000s.
Before we called them components
we had includes.
For me
they served a similar purpose:
reusable snippets of HTML,
generating markup from data.
In most of these languages,
the simplest pattern is a
one-liner with parameters:
In both cases,
we’re passing in values rather than markup.
And that’s the entire point, right?
The purpose of the component
is to generate the markup for us.
All that’s new
with frameworks and web components
is the HTML-looking
element-and-attributes syntax.
Meanwhile,
the articles above
all push for nearly the opposite approach
to ‘HTML web components’.
The suggestion is to wrap custom elements
around normal HTML,
and then enhance those elements
with JavaScript superpowers.
No shadow DOM required,
though it may be sprinkled over the top
in some cases.
The point is to provide an ‘HTML-like’ use
of the components,
which doesn’t rely on shadow DOM as the baseline.
We pass in markup rather than raw values.
Eric Meyer provides a great side-by-side comparison
with his super-slider:
<!-- an 'empty' component with attributes --><super-slidertype="range"min="0.5"max="4"step="0.1"value="2"unit="em"target=".preview h1">
Slider Label
</super-slider><!-- an 'HTML-like' component, with nested elements --><super-sliderunit="em"target=".preview h1"><labelfor="title-size">Slider Label</label><inputid="title-size"type="range"min="0.5"max="4"step="0.1"value="2"/></super-slider>
In this case,
the custom element doesn’t (necessarily)
generate anything at all.
The content is all there in the light DOM, on load,
before any JavaScript gets involved.
These aren’t templates,
they’re markup.
Like the button, form, and a elements –
they don’t have to generate anything new to be useful,
only provide additional semantics and behavior.
Keith also points out
that the CSS cascade
handles web component styles
exactly opposite
from the way frameworks do.
In a framework,
‘scoped’ styles get priority
over page defaults.
The expectation is that
one will build on the other,
similar to how specificity works.
With shadow DOM,
‘encapsulated’ styles are the default.
It takes some work
for the page to get involved
using the ::part() pseudo-class,
but when there’s a conflict
the page always wins.
That’s similar to user agent defaults
provided by the browser –
which makes a lot of sense
if you plan to distribute a new element
for other authors to use
in an unpredictable context.
But it makes less sense
when you just want to wrap up
the ‘card’ styles on your site
into a well-contained snippet.
Custom Elements
seem designed for providing
(wait for it)
custom elements (markup),
while framework components
(old and new)
put more focus
on combining all the elements
into reusable templates.
I don’t think one of those goals is right
and the other one wrong
(setting aside SPA implementation issues) –
but it seems clear
that these tools were designed
to solve different use-cases.
I imagine there’s a counterpoint in here somewhere,
and I just haven’t landed on it.
Maybe we should all be thinking of
component libraries
as ‘third party’ tools,
even when they come from inside the house?
Maybe there’s an architectural benefit
to treating template-components
the same way we treat markup-components?
Certainly there are use-cases
that don’t fall neatly into one category or the other.
I like the HTML-familiar
custom element shape
that’s recommended by these articles.
I think it’s a good idea
to use the light DOM
for content,
even when you’re providing
more shadow DOM structure
behind the scenes.
I was even inspired
by Eric’s <super-slider>
to build something similar of my own.
It’s still experimental,
and I still have a lot to learn
(how do JS classes work?),
but I expect to use this
on my personal site in the future:
But I was surprised to find
this approach is even more JS-forward
than my previous experiments:
Start without any template or slots or shadow DOM
Don’t write any HTML or CSS
It’s all just JavaScript
Even inside the JavaScript, there’s no HTML
Maybe inject inline CSS, or maybe not
More JavaScripting
Only JavaScripts
Title the article “HTML Web Components”
It’s a strange argument,
but the results are compelling –
single-purpose and broadly reusable custom elements
that augment HTML with extra behavior
without adding a lot of overhead.
I think those same
API design principles of ‘HTML components’
can be applied
to a template-driven approach.
Let the light DOM handle content wherever possible.
So I expect I’ll keep exploring
elements defined entirely in HTML,
with auto-registration.
Zach Leatherman suggested
I turn my looping snippet into
a custom element that registers
custom elements:
The next step might be allowing
a nested script tag to contain
a class definition,
for more customization?
I do enjoy recursion,
so this is a fun experiment
even if it doesn’t end up in production.
Maybe I should also aim
my ground-control elements
at each other
(or at the register-element naming attribute)
and see what happens.
It seems like the downside
of writing HTML in HTML
with the <template> tag is
getting it into a website.
Even within my own projects,
I would have to render the template instructions
on every page where they’re needed.
HTML imports never happened.
I imagine that’s why
every example I’ve seen
wraps the template in a JavaScript module,
which can be easily imported
on any page.
So now HTML imports are entirely JS.
Declarative shadow DOM
doesn’t really seem to help that problem.
While it will provide some
other benefits,
it requires even more duplication –
which makes the templating use-case less attractive.
At this point,
tools like WebC
seem to be the best path forward.
All of that leaves me
simultaneously excited
to keep exploring the space,
and frustrated about how much JS
is required
to write even the simplest
‘components’ in my HTML.
And in many cases,
I would still want a build step
on top of
the web platform functionality.
But I’m not sure what I expected.
If I want raw-data flow-control,
I want something that isn’t markup.
Something to generate the markup.
I’ve said for some time
that Sass isn’t going away.
Sass (and other pre-processors)
can still provide structured abstractions of CSS
on the server,
in ways that CSS may never replicate.
Sass can write CSS.
Maybe the same is true with HTML components,
and I shouldn’t expect HTML to write itself.
I don’t think I’m saying anything new here –
these same points seem to come up often.
I’m just stubbing my toe
on a well-documented and well-marked stone.
Here’s Zach,
walking us through the steps:
Hmmm, yeah. He already said it. I bet others have to. Why did I write this?
Zach even comes to
a similar conclusion:
These features are promising,
but they don’t (yet) provide
the DRY solution we’re all hoping for.
I’ve seen that talk before,
but it was before my own toe-stubbing incident.
At some point,
we must all stub our own toes.
I still haven’t even looked into
tools like Lit,
which seem to come highly recommended.
I wonder what those are for.
Do I need even more JS in my JS
to get that super-powered
HTML into my HTML?
How deep does the
imperative language conspiracy go?
For many years, it has been ‘best practice’ to use relative units (especially em and rem) for sizing text. That’s great! But after playing around with my user preferences, I think we can improve on the common approaches.
It is frustrating to track down why an anchor isn’t being found. I’ve found a simple way that should work in most cases. If that doesn’t work, step through the checklist, and then dive in to get a better understanding of how Anchor Positioning works.