WebC
is the new-hotness component framework from all-american web
developer Zach Leatherman. It's described as a standards-based web
component framework for SSGs like
Zach's own eleventy. I've been casually following WebC news since it's
announcement in 2022, and a recent blog post by Bryce Wray conviced me
to try migrating my personal site from nunjucks templates. This post collects
some of my initial impressions from that process.
My goal with this post is to proffer a hearty firgun to Zach and the WebC contributors, and to draw attention to some of the less-stellar experiences I had, for the purposes of improving WebC for everyone. If the post comes off as dismissive or overly-negative, please attribute that to my own lack of insight and tact, rather than to the WebC authors' engineering.
👉 tl;dr: WebC is an exciting technology that comes with shiny tools, but should be adopted by web component developers with caveats. 👈
Advantages
WebC's main advantages in my mind are it's mostly-html
-ish syntax, and
more-or-less semantically correct use of <template>
elements.
I enjoyed the use of scoped framework attributes, where other frameworks would opt to squat on a set of framework attrs in the global scope.
<li for="thingy of thingies">🙄</li>
<li webc:for="thingy of thingies">👨🍳🤌 💋</li>
WebC as a framework at least pays lip service to web standards, which is incredibly refreshing, although as I'll write below I have some reservations about some of the details.
WebC comes preloaded with some very cool css
and js
bundling features, which
help authors split up subresources into buckets that can be loaded as needed.
WebC works with standard custom-elements and provides an opinionated set of guard rails and tools to allow you to register webc-defined custom elements on your pages.
WebC's render templates are a crucial and highly flexible escape hatch which enable piecemeal migration in some cases and advanced templating features. Good stuff!
Bugs and Missing (IMO) Features
While porting over my SVG sprite sheets from nunjucks shortcodes to 11ty components, I ran into some issues with host attributes and the data cascade
@attributes
@attributes
is documented to forward HTML attributes from the
"host" element (i.e. virtual component) to the "root", but in
practice, it turns aria-hidden="true"
into ariahidden="true"
.
This strikes me as a bug or oversight which I imagine the maintainers will be
eager to fix.
Component Data
A common pattern in nunjucks templates uses the {% set %}
tag to compute some local data in scope, that can later be accessed
elsewhere in the template. WebC has a concept of setup scripts
that let you compute some component-local data, but those scripts can't access
the data cascade or the components "props".
This severely limits setup scripts usefulness compared to set
. hopefully the
maintainers will take an interest in covering this case.
Scoping Data to a Subtree
It would be super-cool to be able to scope a bit of data to a subtree. This could solve some of the needs of the previous section, by computing a prop once and using it within its children multiple times.
It's possible to do this by creating a one-off component for the subtree, but that seems overkill - not everything is a component.
Gripes
And now, the things I'm eager to be proven wrong about:
Templating
WebC innovates a novel interpolation syntax, which is fine. To my taste, I found
the syntax serviceable but slightly awkward. I would have prefered js template
literal interpolation syntax like src="${someVarInScope}"
over
:src="someVarInScope"
. Some editors like my preferred NeoVim can use
treesitter to switch to ECMAScript grammar inside the interpolation sections of
HTML attributes when ${}
is present. Using established syntax would have
reduced cognitive and tooling load. This is, admittedly, a minor point of
bikeshedding, and I could understand an argument like "but if it's not
javascript, don't use JS syntax".
Using async data (i.e. promises) in templates can be awkward. If you pass a
promise in as a "prop", you'll have to pass it through async functions in each
interpolation, and won't be able to rely on subsidiary properties like the
length
of a Promise<Array>
. Need to toggle a class on an element if it's
Promise-wrapped data meets a certain predicate? You can use a render template,
move your content (i.e. the class names) to a helper function outside the
template, or calculate your component's private state in the parent template,
but you won't be able to list.length > 12
, because list
is a promise. this
wouldn't be so bad if you could use await
in template expressions, but you
can't.
Render Templates
WebC in it's current condition relies too much on render templates if you want to do anything fancy. As I wrote above, these are a great feature, but for me, I prefer to stick to a single templating language whenever possible - this lightens the load on helpful editor features like treesitter highlighting and LSP.
The major drawback of WebC's JavaScript render templates are that they break
normal JavaScript semantics by eschewing return
and export
. Instead, render
templates render the last expression found within them, a unique behaviour
which strikes me as weird and magical.
Setup Scripts
The way WebC setup scripts works is weird, once again breaking expectations.
They magically assign globals to your component scope, so if you want to name
some intermediary variables you'll have to resort to hacks like hoisting a let
our of a plain block. Which brings me to my biggest, boldest question marks
about WebC.
Standards and Interop
A contemporary, from-scratch web framework should make it easier and smoother to write, bundle, and optimize the standard scoping mechanisms: modules and Shadow DOM. WebC's opinions seem to be directing the user in the opposite direction, though, back to global scope, script parsing mode, and global CSS.
If you want to use modules or shadow DOM in WebC you can, unlike some other popular web frameworks and bundling tools I could mention. WebC won't block you entirely,but it won't exactly help you either, and taken as a whole it seems to me that WebC "wants" you to keep everything in scripts and global css, and "punishes" you for reaching for the standard encapsulation mechanisms - es modules and Shadow DOM.
Modules
As mentioned above, one of WebC's major selling points are the bundling features. But as of this writing, those features only work for global CSS and scripts, but not for Shadow DOM and modules.
If you try to use type="module"
and features like TLA in this render template, you'll get an error:
<script type="module" webc:type="js">
const likes = await getWebmentionLikes(mentions);
const reposts = await getWebmentionReposts(mentions);
'';
</script>
Check the webc:type="js" element in ./_includes/post.webc. Original error message: await is only valid in async functions and the top level bodies of modules (via Error)
It's hard for me to understand the thought process behind picking CommonJS/script parsing for a contemporary green-field web-and-JS-based language. Everything new should be standard modules, barring an extremely compelling reason. Hopefully, Eleventy 3.0's planned support for standard modules will improve the situation for WebC as well.
Shadow DOM
As someone who's built a career around standard web components, I was surprised
to find that the :host
selector had no effect in my WebC components. This is
one place where compiler magic could be used to provide a standard-like
experience where in fact standards (i.e. shadow DOM) aren't being used.
Another instance where WebC's opinions cause confusion is with <slot>
. WebC
will compile your slots away unless you specifically opt-out of it's
standards-avoidance behaviour with <slot webc:keep>
. I'd prefer to see the
sense of webc:keep
reversed so that users are always opting-in to magic
features, instead of opting out to use standard features.
As WebC components are not actually web components, users should be careful when
using :defined
. For example, this common pattern breaks down with WebC.
:not(:defined) {
opacity: 0;
}
WebC's docs suggest that it's possible to output Declarative Shadow DOM templates, and indeed, if you dig around a bit, you can find an example use. Despite WebC authors' apparent enthusiasm for the DSD standard in their docs, I can't shake the feeling that they strongly recommend against using this new standard (at least until Mozilla implements). I'd prefer for DSD to be a first-class output target for WebC, with HTML templates printed as Shadow DOM and CSS printed into DSD templates. This is the very purpose of the spec, and I think a more full-throated adoption of DSD in WebC would go a long way to alleviating interop concerns.
Interop
How useful is WebC outside of 11ty, and outside of other WebC projects? Compared to more established web components frameworks, the emphasis here seems to be more on the "component" aspect and less on the "web" (as far as interops with other web technologies) aspect.
I'm concerned about how realistic it will be to interop between webc and other web components. First, not every WebC "component" is actually a custom-element, and in fact WebC's recommendations and guidelines seem to advise against using web components technologies like custom elements and Shadow DOM in most cases.
Reading the ledes of WebC docs and hype posts, you might be forgiven for thinking that WebC is all about writing web components, but based on my experiences so far, I think a fairer characterization is that WebC is a javascript server framework that (blessedly) isn't entirely hostile to web components.
Wrapup
Although I have a lot to say about what I personally perceive as WebC's current lackings, please dear reader don't take that as a dismissal or criticism of WebC as an emerging technology. I believe the language has a really nice potential, and if it's stated goal of standards-based SSG authoring sometimes falls short in my view, it should still be lauded and encouraged. Having finished my first rodeo with WebC I'm confident in continuing to learn and invest in the technology, albeit with some caveats with regards to interop and standard web components.
So who is WebC for? If you're an 11ty user familiar with nunjucks, starting a new project with no intention of using javasscript or web components, WebC is an appropriate choice to adopt. If you're migrating a large njk 11ty site, perhaps wait for the WebC APIs to mature, or adopt it in limited and controlled areas of your site. If you're a web component developer, adopt WebC as a server-side templating language, but not (yet) as a web component framework.
If you've spotted any errors or omissions in this post, or would like to prove me wrong about my gripes, please don't hesitate to reach out on mastodon. I'd really like to be corrected on any misconceptions here in this post and will add an "errata" section for any I come across.