6 posts on Web Components

Inline conditionals in CSS, now?

20 min read 0 comments Report broken page

The CSS WG resolved to add if() to CSS, but that won’t be in browsers for a while. What are our options in the meantime?

A couple days ago, I posted about the recent CSS WG resolution to add an if() function to CSS. Great as it may be, this is still a long way off, two years if everything goes super smoothly, more if not. So what can you do when you need conditionals right now?

You may be pleased to find that you’re not completely out of luck. There is a series of brilliant, horrible hacks that enable you to expose the kinds of higher level custom properties that conditionals typically enable.

Using hacks in production?!

The instinctive reaction many developers have when seeing hacks like these is “Nice hack, but can’t possibly ever use this in production”. This sounds reasonable on the surface (keeping the codebase maintainable is a worthy goal!) but when examined deeply, it reflects the wrong order of priorities, prioritizing developer convenience over user convenience.

The TAG maintains a Web Platform Design Principles document [1] that everyone designing APIs for the web platform is supposed to read and follow. I’m a strong believer in having published Design Principles, for any product[2]. They help stay on track, and remember what the big picture vision is, which is otherwise easy to lose sight of in the day to day minutiae. One of the core principles in the document is the Priority of Constituencies. The core of it is:

User needs come before the needs of web page authors, which come before the needs of user agent implementors, which come before the needs of specification writers, which come before theoretical purity.

Obviously in most projects there are far fewer stakeholders than for the whole web platform, but the spirit of the principle still applies: the higher the abstraction, the higher priority the user needs. Or, in other words, consumers above producers.

For a more relatable example, in a web app using a framework like e.g. Vue and several Vue components, the user needs of website users come before the needs of the web app developers, which come before the needs of the developers of its Vue components, which come before the needs of the Vue framework developers (sorry Evan :).

The TAG did not invent this principle; it is well known in UX and Product circles with a number of different wordings:

  • “Put the pain on those who can bear it”
  • Prefer internal complexity over external complexity

Why is that? Several reasons:

  • It is far easier to change the implementation than to change the user-facing API, so it’s worth making sacrifices to keep it clean from the get go.
  • Most products have way more users than developers, so this minimizes collective pain.
  • Internal complexity can be managed far more easily, with tooling or even good comments.
  • Managing complexity internally localizes it and contains it better.
  • Once the underlying platform improves, only one codebase needs to be changed to reap the benefits.

The corollary is that if hacks allow you to expose a nicer API to component users, it may be worth the increase in internal complexity (to a degree). Just make sure that part of the code is well commented, and keep track of it so you can return to it once the platform has evolved to not require a hack anymore.

Like all principles, this isn’t absolute. A small gain in user convenience is not a good tradeoff when it requires tremendous implementation complexity. But it’s a good north star to follow.

As to whether custom properties are a better option to control styling than e.g. attributes, I listed several arguments for that in my previous article. Although, there are also cases where using custom properties is not a good idea…

When is it not a good idea to do this?

In a nutshell, when the abstraction is likely to leak. Ugliness is only acceptable if it’s encapsulated and not exposed to component users. If there is a high chance they may come into contact with it, it might be a better idea to simply use attributes and call it a day.

A series of callouts with --variant declarations next to them

Example callouts with three variants.

In many of the examples below, I use variants as the canonical example of a custom property that a component may want to expose. However, if component consumers may need to customize each variant, it may be better to use attributes so they can just use e.g. [variant="success"] instead of having to understand whatever crazy hack was used to expose a --variant custom property. And even from a philosophical purity perspective, variants are on the brink of presentational vs semantic anyway.

The current state of the art

There is a host of hacks and workarounds that people have come up with to make up for the lack of inline conditionals in CSS, with the first ones dating back to as early as 2015.

1. Binary Linear Interpolation

This was first documented by Roma Komarov in 2016, and has since been used in a number of creative ways. The gist of this method is to use essentially the linear interpolation formula for mapping [0,1] to [a,b]:


However, instead of using this to map a range to another range, we use it to map two points to two other points, basically the two extremes of both ranges: p=0 and p=1 to select a and b respectively.

This was Roma’s original example:

:root {
		--is-big: 0;

.is-big { –is-big: 1; }

.block { padding: calc( 25px * var(–is-big) + 10px * (1 - var(–is-big)) ); border-width: calc( 3px * var(–is-big) + 1px * (1 - var(–is-big)) ); }

He even expands it to multiple conditions by multiplying the interpolation factors. E.g. this code snippet to map 0 to 100px, 1 to 20px, and 2 to 3px:

.block {
		padding: calc(
				100px * (1 - var(--foo)) * (2 - var(--foo)) * 0.5 +
				20px  * var(--foo) * (2 - var(--foo)) +
				3px   * var(--foo) * (1 - var(--foo)) * -0.5

Which these days could be rewritten as this, which also makes the boolean logic at play clearer:

.block {
		--if-not-0: min(max(0 - var(--foo), var(--foo) - 0), 1);
		--if-not-1: min(max(1 - var(--foo), var(--foo) - 1), 1);
		--if-not-2: min(max(2 - var(--foo), var(--foo) - 2), 1);

–if-0: var(–if-not-1) * var(–if-not-2); –if-1: var(–if-not-0) * var(–if-not-2); –if-2: var(–if-not-0) * var(–if-not-1);

padding: calc( 100px * var(–if-0) + 20px * var(–if-1) + 3px * var(–if-2) ); }

Back then, min() and max() were not available, so he had to divide each factor by an obscure constant to make it equal to 1 when it was not 0. Once abs() ships this will be even simpler (the inner max() is basically getting the absolute value of N - var(--foo))

Ana Tudor also wrote about this in 2018, in this very visual article: DRY Switching with CSS Variables. Pretty sure she was also using boolean algebra on these too (multiplication = AND, addition = OR), but I couldn’t find the exact post.

2. Toggles (Space Toggle, Cyclic Toggles)

This was independently discovered by Ana Tudor (c. 2017), Jane Ori in April 2020 (who gave it the name “Space Toggle”), David Khoursid (aka David K Piano) in June 2020 (he called it prop-and-lock), and yours truly in Oct 2020 (I called it the --var: ; hack, arguably the worst name of the three 😅).

The core idea is that var(--foo, fallback) is actually a very limited form of conditional: if --foo is initial (or IACVT), it falls back to fallback, otherwise it’s var(--foo). Furthermore, we can set custom properties (or their fallbacks) to empty values to get them to be ignored when used as part of a property value. It looks like this:

:root {
	--if-success: ;
	--if-warning: ;
.success {
	--if-success: initial;

.warning { –if-warning: initial; }

.callout { background: var(–if-success, var(–color-success-90)) var(–if-warning, var(–color-warning-90)); }

One of the downsides of this version is that it only supports two states per variable. Note how we needed two variables for the two states. Another downside is that there is no way to specify a fallback if none of the relevant variables are set. In the example above, if neither --if-success nor --if-warning are set, the background declaration will be empty, and thus become IACVT which will make it transparent.

Cyclic Toggles

In 2023, Roma Komarov expanded the technique into what he called “Cyclic Dependency Space Toggles” which addresses both limitations: it supports any number of states, and allows for a default value. The core idea is that variables do not only become initial when they are not set, or are explicitly set to initial, but also when cycles are encountered.

Roma’s technique depends on this behavior by producing cycles on all but one of the variables used for the values. It looks like this:

.info {
	--variant: var(--variant-default);

–variant-default: var(–variant,); –variant-success: var(–variant,); –variant-warning: var(–variant,); –variant-error: var(–variant,);

background: var(–variant-default, lavender) var(–variant-success, palegreen) var(–variant-warning, khaki) var(–variant-error, lightpink); }

And is used like this:

.my-warning {
	--variant: var(--variant-warning);

A downside of this method is that since the values behind the --variant-success, --variant-warning, etc variables are specific to the --variant variable they need to be namespaced to avoid clashes.

Layered Toggles

A big downside of most of these methods (except for the animation-based ones) is that you need to specify all values of the property in one place, and the declaration gets applied whether your custom property has a value or not, which makes it difficult to layer composable styles leading to some undesirable couplings.

Roma Komarov’s “Layered Toggles” method addresses this for some cases by allowing us to decouple the different values by taking advantage of Cascade Layers. The core idea is that Cascade Layers include a revert-layer keyword that will cause the current layer to be ignored wrt the declaration it’s used on. Given that we can use unnamed layers, we can simply user a @layer {} rule for every block of properties we want to apply conditionally.

This approach does have some severe limitations which made it rather unpractical for my use cases. The biggest one is that anything in a layer has lower priority than any unlayered styles, which makes it prohibitive for many use cases. Also, this doesn’t really simplify cyclic toggles, you still need to set all values in one place. Still, worth a look as there are some use cases it can be helpful for.

3. Paused animations

The core idea behind this method is that paused animations (animation-play-state: paused) can still be advanced by setting animation-delay to a negative value. For example in an animation like animation: 100s foo, you can access the 50% mark by setting animation-delay: -50s. It’s trivial to transform raw numbers to <time> values, so this can be abstracted to plain numbers for the user-facing API.

Here is a simple example to illustrate how this works:

@keyframes color-mixin {
	0% { background: var(--color-neutral-90); border-color: var(--color-neutral-80); }
	25% { background: var(--color-success-90); border-color: var(--color-success-80); }
	50% { background: var(--color-warning-90); border-color: var(--color-warning-80); }
	75% { background: var(--color-danger-90); border-color: var(--color-danger-80); }

button { animation: foo 100s calc(var(–variant) * -100s / 4 ) infinite paused; }

Used like:

.error button {
	--variant: 2;

This is merely to illustrate the core idea, having a --variant property that takes numbers is not a good API! Though the numbers could be aliased to variables, so that users would set --variant: var(--success).

This technique seems to have been first documented by me in 2015, during a talk about …pie charts (I would swear I showed it in an earlier talk but I cannot find it). I never bothered writing about it, but someone else did, 4 years later.

To ensure you don’t get slightly interpolated values due to precision issues, you could also slap a steps() in there:

button {
	animation: foo 100s calc(var(--variant) * -100s / 4 ) infinite paused steps(4);

This is especially useful when 100 divided by your number of values produces repeating decimals, e.g. 3 steps means your keyframes are at increments of 33.33333%.

A benefit of this method is that defining each state is done with regular declarations, not involving any weirdness, and that .

It does also have some obvious downsides:

  • Values restricted to numbers
  • Takes over the animation property, so you can’t use it for actual animations.

4. Type Grinding

So far all of these methods impose constraints on the API exposed by these custom properties: numbers by the linear interpolation method and weird values that have to be hidden behind variables for the space toggle and cyclic toggle methods.

In October 2022, Jane Ori was the first one to discover a method that actually allows us to support plain keywords, which is what the majority of these use cases needs. She called it “CSS-Only Type Grinding”.

Its core idea is if a custom property is registered (via either @property or CSS.registerProperty()), assigning values to it that are not valid for its syntax makes it IACVT (Invalid at computed value time) and it falls back to its initial (or inherited) value.

She takes advantage of that to progressively transform keywords to other keywords or numbers through a series of intermediate registered custom properties, each substituting one more value for another.

I was recently independently experimenting with a similar idea. It started from a use case of one of my components where I wanted to implement a --size property with two values: normal and large. Style queries could almost get me there, but I also needed to set flex-flow: column on the element itself when --size was large.

The end result takes N + 1 @property rules, where N is the number of distinct values you need to support. The first one is the rule defining the syntax of your actual property:

@property --size {
	syntax: "normal | large",
	initial-value: normal;
	inherits: true;

Then, you define N more rules, each progressively substituting one value for another:

@property --size-step-1 {
	syntax: "row | large";
	initial-value: row;
	inherits: false;

@property --size-step-end { syntax: "row | column"; initial-value: column; inherits: false; }

And at the component host you daisy chain them like this:

:host {
	--size-step-1: var(--size);
	--size-step-end: var(--size-step-1);
	flex-flow: var(--size-step-end);

And component consumers get a really nice API:

.my-component {
	--size: large;

You can see it in action in this codepen:

See the Pen Transform keywords to other keywords (2 keyword version) by Lea Verou (@leaverou) on CodePen.

You can use the same general idea to transform more keywords or to transform keywords into different sets of keywords for use in different properties.

We can also transform keywords to numbers, by replacing successive keywords with <integer> in the syntax, one at a time, with different initial values each time. Here is the --variant example using this method:

@property --variant {
	syntax: "none | success | warning | danger";
	initial-value: none;
	inherits: true;

@property --variant-step-1 { syntax: "none | <integer> | warning | danger"; initial-value: 1; inherits: false; }

@property --variant-step-2 { syntax: "none | <integer> | danger"; initial-value: 2; inherits: false; }

@property --variant-step-3 { syntax: "none | <integer>"; initial-value: 3; inherits: false; }

@property --variant-index { syntax: "<integer>"; initial-value: 0; inherits: false; }

.callout { –variant-step-1: var(–variant); –variant-step-2: var(–variant-step-1); –variant-step-3: var(–variant-step-2); –variant-index: var(–variant-step-3);

/* Now use --variant-index to set other values */ }

Then, we can use techniques like linear range mapping to transform it to a length or a percentage (generator) or recursive color-mix() to use that number to select an appropriate color.

5. Variable animation name

In 2018, Roma Komarov discovered another method that allows plain keywords to be used as the custom property API, forgot about it, then rediscovered it in June 2023 😅. He still never wrote about it, so these codepens are the only documentation we have. It’s a variation of the previous method: instead of using a single @keyframes rule and switching between them via animation-delay, define several separate @keyframes rules, each named after the keyword we want to use:

@keyframes success {
	from, to {
		background-color: var(--color-success-90);
		border-color: var(--color-success-80);
@keyframes warning {
	from, to {
		background-color: var(--color-warning-90);
		border-color: var(--color-warning-80);
@keyframes danger {
	from, to {
		background-color: var(--color-danger-90);
		border-color: var(--color-danger-80);

.callout { padding: 1em; margin: 1rem; border: 3px solid var(–color-neutral-80); background: var(–color-neutral-90);

animation: var(–variant) 0s paused both; }

Used like:

.warning {
	--variant: warning;

The obvious downsides of this method are:

  • Impractical to use outside of Shadow DOM due to the potential for name clashes.
  • Takes over the animation property, so you can’t use it for actual animations.


Every one of these methods has limitations, some of which are inerent in its nature, but others can be improved upon. In this section I will discuss some improvements that me or others have thought of. I decided to include these in a separate section, since they affect more than one method.

Making animation-based approaches cascade better

A big downside with the animation-based approaches (3 and 5) is the place of animations in the cascade: properties applied via animation keyframes can only be overridden via other animations or !important.

One way to deal with that is to set custom properties in the animation keyframes, that you apply in regular rules. To use the example from Variable animation name:

@keyframes success {
	from, to {
		--background-color: var(--color-success-90);
		--border-color: var(--color-success-80);
@keyframes warning {
	from, to {
		--background-color: var(--color-warning-90);
		--border-color: var(--color-warning-80);
@keyframes danger {
	from, to {
		--background-color: var(--color-danger-90);
		--border-color: var(--color-danger-80);

.callout { padding: 1em; margin: 1rem; border: 3px solid var(–border-color, var(–color-neutral-80)); background-color: var(–background-color, var(–color-neutral-90));

animation: var(–variant) 0s paused both; }

Note that you can combine the two approaches (variable animation-name and paused animations) when you have two custom properties where each state of the first corresponds to N distinct states of the latter. For example, a --variant that sets colors, and a light/dark mode within each variant that sets different colors.

Making animation-based approaches compose better with author code

Another downside of the animation-based approaches is that they take over the animation property. If authors want to apply an animation to your component, suddenly a bunch of unrelated things stop working, which is not great user experience.

There isn’t that much to do here to prevent this experience, but you can at least offer a way out: instead of defining your animations directly on animation, define them on a custom property, e.g. --core-animations. Then, if authors want to apply their own animations, they just make sure to also include var(--core-animations) before or after.

Discrete color scales

Many of the approaches above are based on numerical values, which are then mapped to the value we actually want. For numbers or dimensions, this is easy. But what about colors?

I linked to Noah Liebman’s post above on recursive color-mix(), where he presents a rather complex method to select among a continuous color scale based on a 0-1 number.

However, if you don’t care about any intermediate colors and just want to select among a few discrete colors, the method can be a lot simpler. Simple enough to be specified inline.

Let me explain: Since color-mix() only takes two colors, we need to nest them to select among more than 2, no way around that. However, the percentages we calculate can be very simple: 100% when we want to select the first color and 0% otherwise. I plugged these numbers into my CSS range mapping tool (example) and noticed a pattern: If we want to output 100% when our variable (e.g. --variant-index) is N-1 and 0% when it’s N, we can use 100% * (N - var(--variant-index)).

We can use this on every step of the mixing:

background: color-mix(in oklab,
	var(--stone-2) calc(100% * (1 - var(--color-index, 0))), /* default color */
	color-mix(in oklab,
		var(--green-2) calc(100% * (2 - var(--color-index))),
		color-mix(in oklab,
			var(--yellow-2) calc(100% * (3 - var(--color-index))),

But what happens when the resulting percentage is < 0% or > 100%? Generally, percentages outside [0%, 100%] make color-mix() invalid, which would indicate that we need to take care to keep our percentages within that range (via clamp() or max()). However, within math functions there is no parse-time range-checking, so values are simply clamped to the allowed range.

Here is a simple example that you can play with (codepen):

See the Pen Discrete color scales with simpler recursive color-mix() by Lea Verou (@leaverou) on CodePen.

And here is a more realistic one, using the Type Grinding method to transform keywords to numbers, and then using the above technique to select among 4 colors for backgrounds and borders (codepen).

Combining approaches

There are two components to each method: the input values it supports, i.e. your custom property API that you will expose, e.g. numbers, keywords, etc., and the output values it supports (<dimension>, keywords, etc.).

Even without doing anything, we can combine methods that support the same type of input values, e.g. Binary Linear Interpolation and Paused animations or Type Grinding and Variable animation names.

If we can transform the input values of one method to the input values of another, we can mix and match approaches to maximize flexibility. For example, we can use type grinding to transform keywords to numbers, and then use paused animations or binary linear interpolation to select among a number of quantitative values based on that number.

Keywords → Numbers
Type grinding
Numbers → Keywords
We can use paused animations to select among a number of keywords based on a number (which we transform to a negative animation-delay).
Space toggles → Numbers
Easy: --number: calc(0 var(--toggle, + 1))
Numbers → Space toggles
Once again, Roma Komarov has come up with a very cool trick: he conditionally applies an animation which interpolates two custom properties from initial to the empty value and vice versa — basically variable animation names but used on an internal value. Unfortunately a Firefox bug prevents it from working interoperably. He also tried a variant for space toggles but that has even worse compatibility, limited to Chrome only. I modified his idea a bit to use paused animations instead, and it looks like my attempt works on Firefox as well. 🎉

So, which one is better?

I’ve summarized the pros and cons of each method below:

Method Input values Output values Pros Cons

Binary Linear Interpolation

Numbers Quantitative
  • Lightweight
  • Requires no global rules
  • Limited output range


var(--alias) (actual values are too weird to expose raw)

  • Can be used in part of a value
  • Weird values that need to be aliased

Paused animations

Numbers Any
  • Normal, decoupled declarations
  • Takes over animation property
  • Cascade weirdness

Type Grinding


Any value supported by the syntax descriptor

  • High flexibility for exposed API
  • Good encapsulation
  • Must insert CSS into light DOM
  • Tedious code (though can be automated with build tools)
  • No Firefox support (though that’s changing)

Variable animation name

Keywords Any
  • Normal, decoupled declarations
  • Impractical outside of Shadow DOM due to name clashes
  • Takes over animation property
  • Cascade weirdness

The most important consideration is the API we want to expose to component users. After all, exposing a nicer API is the whole point of this, right?

If your custom property makes sense as a number without degrading usability (e.g. --size may make sense as a number, but small | medium | large is still better than 0 | 1 | 2), then Binary Linear Interpolation is probably the most flexible method to start with, and as we have seen in Combining approaches section, numbers can be converted to inputs for every other method.

However, in the vast majority of cases I have seen, the north star API is a set of plain, high-level keywords. This is only possible via Type Grinding and Variable animation names.

Between the two, Type Grinding is the one providing the best encapsulation, since it relies entirely on custom properties and does not hijack any native properties.

Unfortunately, the fact that @property is not yet supported in Shadow DOM throws a spanner in the works, but since these intermediate properties are only used for internal calculations, we can just give them obscure names and insert them in the light DOM.

On the other hand, @keyframes are not only allowed, but also properly scoped when used in Shadow DOM, so Variable animation name might be a good choice when you don’t want to use the same keywords for multiple custom properties on the same component and its downsides are not dealbreakers for your particular use case.


Phew! That was a long one. If you’re aware of any other techniques, let me know so I can add them.

And I think after all of this, if you had any doubt that we need if() in CSS, the sheer number and horribleness of these hacks must have dispelled it by now. 😅

Thanks to Roma Komarov for reviewing earlier drafts of this article.

  1. I’ve always thought this was our most important deliverable, and pushed for prioritizing it. Recently, I even became editor of it. 🙃 ↩︎

  2. I’m using product here in the general sense, of any software product, technology, or API, not just for-profit or commercial ones. ↩︎

Inline conditionals in CSS?

6 min read 0 comments Report broken page

Last week, the CSS WG resolved to add an inline if() to CSS. But what does that mean, and why is it exciting?

Last week, we had a CSS WG face-to-face meeting in A Coruña, Spain. There is one resolution from that meeting that I’m particularly excited about: the consensus to add an inline if() to CSS. While I was not the first to propose an inline conditional syntax, I did try and scope down the various nonterminating discussions into an MVP that can actually be implemented quickly, discussed ideas with implemenators, and eventually published a concrete proposal and pushed for group resolution. Quite poetically, the relevant discussion occurred on my birthday, so in a way, I got if() as the most unique birthday present ever. 😀

This also comes to show that proposals being rejected is not the end-all for a given feature. It is in fact quite common for features to be rejected for several times before they are accepted: CSS Nesting, :has(), container queries were all simply the last iteration in a series of rejected proposals. if() itself was apparently rejected in 2018 with very similar syntax to what I proposed. What was the difference? Style queries had already shipped, and we could simply reference the same syntax for conditions (plus media() and supports() from Tab’s @when proposal) whereas in the 2018 proposal how conditions would work was largely undefined.

I posted about this on a variety of social media, and the response by developers has been overwhelmingly positive:

I even had friends from big companies writing to tell me their internal Slacks blew up about it. This proves what I’ve always suspected, and was part of the case I made to the CSS WG: that this is a huge pain point. Hopefully the amount and intensity of positive reactions will help browsers prioritize this feature and add it to their roadmaps earlier rather than later.

Across all these platforms, besides the “I can’t wait for this to ship!” sentiment being most common, there were a few other recurring questions and a fair bit of confusion that I figured were worth addressing.

Continue reading

On ratings and meters

2 min read 0 comments Report broken page

I always thought that the semantically appropriate way to represent a rating (e.g. a star rating) is a <meter> element. They essentially convey the same type of information, the star rating is just a different presentation.

An example of a star rating widget, from Amazon

However, trying to style a <meter> element to look like a star rating is …tricky at best. Not to mention that this approach won’t even work in Shadow trees (unless you include the CSS in every single shadow tree).

So, I set out to create a proper web component for star ratings. The first conundrum was, how does this relate to a <meter> element?

  • Option 1: Should it extend <meter> using builtin extends?
  • Option 2: Should it use a web component with a <meter> in Shadow DOM?
  • Option 3: Should it be an entirely separate web component that just uses a meter ARIA Role and related ARIA attributes?

Continue reading

What is the best way to mark up an exclusive button group?

2 min read 0 comments Report broken page

A few days ago I asked Twitter a seemingly simple question (I meant aria-pressed, not aria-selected but Twitter doesn’t allow edits…):

For background, I was implementing a web component for an app I’m working on at work and I was getting into some pretty weird rabbit holes with my approach of generating radios and labels.

Unsurprisingly, most people thought the best solution is radio buttons and labels. After all, it works without CSS, right? Progressive enhancement and everything?

That’s what I thought too. I had contorted my component to generate labels and radios in the Shadow DOM from buttons in the light DOM, which resulted in awkward code and awkward CSS, but I felt I was fighting the good fight and doing the best thing for accessibility.

All this was challenged when the actual accessibility expert, Léonie Watson chimed in. For those of you who don’t know her, she is pretty much the expert when it comes to web accessibility and standards. She is also visually impaired herself, giving her a firsthand experience many other a11y aficionados lack. Her recommendation was contrary to what most others were saying:

Continue reading

On Yak Shaving and <md-block>, a new HTML element for Markdown

2 min read 0 comments Report broken page

This week has been Yak Shaving Galore. It went a bit like this:

  1. I’ve been working on a web component that I need for the project I’m working on. More on that later, but let’s call it <x-foo> for now.
  2. Of course that needs to be developed as a separate reusable library and released as a separate open source project. No, this is not the titular component, this was only level 1 of my multi-level yak shaving… 🤦🏽‍♀️
  3. I wanted to showcase various usage examples of that component in its page, so I made another component for these demos: <x-foo-live>. This demo component would have markup with editable parts on one side and the live rendering on the other side.
  4. I wanted the editable parts to autosize as you type. Hey, I’ve written a library for that in the past, it’s called Stretchy!
  5. But Stretchy was not written in ESM, nor did it support Shadow DOM. I must rewrite Stretchy in ESM and support Shadow DOM first! Surely it won’t take more than a half hour, it’s a tiny library.
  6. (It took more than a half hour)
  7. Ok, now I have a nice lil’ module, but I also need to export IIFE as well, so that it’s compatible with Stretchy v1. Let’s switch to Rollup and npm scripts and ditch Gulp.
  8. Oh look, Stretchy’s CSS is still written in Sass, even though it doesn’t really need it now. Let’s rewrite it to use CSS variables, use PostCSS for nesting, and use conic-gradient() instead of inline SVG data URIs.
  9. Ok, Stretchy v2 is ready, now I need to update its docs. Oooh, it doesn’t have a README? I should add one. But I don’t want to duplicate content between the page and the README. Hmmm, if only…
  10. I know! I’ll make a web component for rendering both inline and remote Markdown! I have an unfinished one lying around somewhere, surely it won’t take more than a couple hours to finish it?
  11. (It took almost a day, two with docs, demos etc)
  12. Done! Here it is! https://md-block.verou.me
  13. Great! Now I can update Stretchy’s docs and release its v2
  14. Great! Now I can use Stretchy in my <x-foo-live> component demoing my <x-foo> component and be back to only one level of yak shaving!
  15. Wow, it’s already Friday afternoon?! 🤦🏽‍♀️😂

Hopefully you find useful! Enjoy!

Original, Personal, Releases, JS, ESM, Markdown, Stretchy, Web Components, Yak Shaving
Edit post on GitHub

The failed promise of Web Components

4 min read 0 comments Report broken page

Web Components had so much potential to empower HTML to do more, and make web development more accessible to non-programmers and easier for programmers. Remember how exciting it was every time we got new shiny HTML elements that actually do stuff? Remember how exciting it was to be able to do sliders, color pickers, dialogs, disclosure widgets straight in the HTML, without having to include any widget libraries?

The promise of Web Components was that we’d get this convenience, but for a much wider range of HTML elements, developed much faster, as nobody needs to wait for the full spec + implementation process. We’d just include a script, and boom, we have more elements at our disposal!

Or, that was the idea. Somewhere along the way, the space got flooded by JS frameworks aficionados, who revel in complex APIs, overengineered build processes and dependency graphs that look like the roots of a banyan tree.

This is what the roots of a Banyan tree look like. Photo by David Stanley on Flickr (CC-BY).

Perusing the components on webcomponents.org fills me with anxiety, and I’m perfectly comfortable writing JS — I write JS for a living! What hope do those who can’t write JS have? Using a custom element from the directory often needs to be preceded by a ritual of npm flugelhorn, import clownshoes, build quux, all completely unapologetically because “here is my truckload of dependencies, yeah, what”. Many steps are even omitted, likely because they are “obvious”. Often, you wade through the maze only to find the component doesn’t work anymore, or is not fit for your purpose.

Besides setup, the main problem is that HTML is not treated with the appropriate respect in the design of these components. They are not designed as closely as possible to standard HTML elements, but expect JS to be written for them to do anything. HTML is simply treated as a shorthand, or worse, as merely a marker to indicate where the element goes in the DOM, with all parameters passed in via JS. I recall a wonderful talk by Jeremy Keith a few years ago about this very phenomenon, where he discussed this e-shop Web components demo by Google, which is the poster child of this practice. These are the entire contents of its <body> element:

	<shop-app unresolved="">SHOP</shop-app>
	<script src="node_assets/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
	<script type="module" src="src/shop-app.js"></script>

If this is how Google is leading the way, how can we hope for contributors to design components that follow established HTML conventions?

Jeremy criticized this practice from the aspect of backwards compatibility: when JS is broken or not enabled, or the browser doesn’t support Web Components, the entire website is blank. While this is indeed a serious concern, my primary concern is one of usability: HTML is a lower barrier to entry language. Far more people can write HTML than JS. Even for those who do eventually write JS, it often comes after spending years writing HTML & CSS.

If components are designed in a way that requires JS, this excludes thousands of people from using them. And even for those who can write JS, HTML is often easier: you don’t see many people rolling their own sliders or using JS-based ones once <input type="range"> became widely supported, right?

Even when JS is unavoidable, it’s not black and white. A well designed HTML element can reduce the amount and complexity of JS needed to a minimum. Think of the <dialog> element: it usually does require *some* JS, but it’s usually rather simple JS. Similarly, the <video> element is perfectly usable just by writing HTML, and has a comprehensive JS API for anyone who wants to do fancy custom things.

The other day I was looking for a simple, dependency free, tabs component. You know, the canonical example of something that is easy to do with Web Components, the example 50% of tutorials mention. I didn’t even care what it looked like, it was for a testing interface. I just wanted something that is small and works like a normal HTML element. Yet, it proved so hard I ended up writing my own!

Can we fix this?

I’m not sure if this is a design issue, or a documentation issue. Perhaps for many of these web components, there are easier ways to use them. Perhaps there are vanilla web components out there that I just can’t find. Perhaps I’m looking in the wrong place and there is another directory somewhere with different goals and a different target audience.

But if not, and if I’m not alone in feeling this way, we need a directory of web components with strict inclusion criteria:

  • Plug and play. No dependencies, no setup beyond including one <script> tag. If a dependency is absolutely needed (e.g. in a map component it doesn’t make sense to draw your own maps), the component loads it automatically if it’s not already loaded.
  • Syntax and API follows conventions established by built-in HTML elements and anything that can be done without the component user writing JS, is doable without JS, per the W3C principle of least power.
  • Accessible by default via sensible ARIA defaults, just like normal HTML elements.
  • Themable via ::part(), selective inheritance and custom properties. Very minimal style by default. Normal CSS properties should just “work” to the the extent possible.
  • Only one component of a given type in the directory, that is flexible and extensible and continuously iterated on and improved by the community. Not 30 different sliders and 15 different tabs that users have to wade through. No branding, no silos of “component libraries”. Only elements that are designed as closely as possible to what a browser would implement in every way the current technology allows.

I would be up for working on this if others feel the same way, since that is not a project for one person to tackle. Who’s with me?

UPDATE: Wow this post blew up! Thank you all for your interest in participating in a potential future effort. I’m currently talking to stakeholders of some of the existing efforts to see if there are any potential collaborations before I go off and create a new one. Follow me on Twitter to hear about the outcome!