Anchor Positioning Is Disruptive
New layouts will be possible
The more I play with it, the more convinced I am that anchor positioning is going to unlock some surprising new layouts.
I’m still getting used to this
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.
Is that the way it has to be?
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>
<slot name="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:
const elementTemplates = document.querySelectorAll('template[data-register]');
elementTemplates.forEach((template) => {
customElements.define(
template.dataset.register,
class extends HTMLElement {
constructor() {
super();
let content = template.content;
const shadowRoot = this.attachShadow({ mode: "open" }).appendChild(
content.cloneNode(true)
);
}
}
);
});
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 -->
<template data-register="media-object">
<style>
/* Media object styles */
</style>
<div class="object">
<div part="media">
<slot name="media"></slot>
</div>
<div part="content">
<slot></slot>
</div>
</div>
</template>
<!-- using the component -->
<media-object>
<img slot="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>
<slot name="title">
<h2>Normal Old HTML, With Slots!</h2>
</slot>
<time @html="formatDate(now)"></time>
<p webc: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.
<figure class="block-quote">
<blockquote @html="md(quoteText)"></blockquote>
<quote-cite
:@cite-name="citeName"
:@cite-date="citeDate"
:@cite-url="citeUrl"
:@cite-source="citeSource"
></quote-cite>
</blockquote>
Miriam comes cautiously out, with Toto under her arm, and looks about. Music comes up slowly.
Miriam (after a pause)
“I’ve got a feeling
we’re not on the web platform any more.”
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:
<!-- Liquid template render -->
{% render 'blockquote', text: quoteText, … %}
<!-- Nunjucks macro -->
{% import "content.macros.njk" as content %}
{{ content.blockquote(
text: quoteText,
…
) }}
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-slider
type="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-slider unit="em" target=".preview h1">
<label for="title-size">Slider Label</label>
<input id="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.
This leads me to agree with Keith Grant when he says that Web Components Aren’t Components.
At least, it’s not the same definition for component that modern frameworks use.
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:
See the Pen Ground Control web component by @undefined on CodePen.
But I was surprised to find this approach is even more JS-forward than my previous experiments:
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:
See the Pen Custom Element Registration Element by @undefined on CodePen.
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:
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?
New layouts will be possible
The more I play with it, the more convinced I am that anchor positioning is going to unlock some surprising new layouts.
Performance, scope, and fallbacks for the anchor positioning polyfill
Our sponsors are supporting the continued development of the CSS Anchor Positioning Polyfill. Here’s a summary of the latest updates.
Are we measuring what we meant to measure?
There’s been a lot of interest in the results of the annual State of CSS survey, but are we asking all the right questions?