Categories
Articles Thoughts

JS private class fields considered harmful

Reading Time: 2 minutes

Today I mourn. What am I mourning? Encapsulation. At least in my projects.

As a library author, I’ve decided to avoid private class fields from now on and gradually refactor them out of my existing libraries.

Why did I make such a drastic decision?

It all started a few days ago, when I was building a Vue 3 app that used Color.js Color objects. For context, Vue 3 uses proxies to implement its reactivity system, just like Mavo did back in 2016 (the first one to do so as far as I’m aware). I was getting several errors and upon tracking them down I had a very sad realization: instances of classes that use private fields cannot be proxied.

I will let that sink in for a bit. Private fields, proxies, pick one, you can’t have both. Here is a reduced testcase illustrating the problem.

Basically, because a Proxy creates a different object, it breaks both strict equality (obj1 === obj2), as well as private properties. MDN even has a whole section on this. Unfortunately, the workaround proposed is no help when proxies are used to implement reactivity, so when I tried to report this as a Vue bug, it was (rightly) closed as wontfix. It would not be possible to fix this in Mavo either, for the same reason.

I joined TC39 fairly recently, so I was not aware of the background when proxies or private class fields were designed. Several fellow TC39 members filled me in on the discussions from back then. A lot of the background is in this super long thread, some interesting tl;drs as replies to my tweet:

After a lot of back and forth, I decided I cannot justify using private properties going forwards. The tradeoff is simply not worth it. There is no real workaround for proxy-ability, whereas for private fields there is always private-by-convention. Does it suck? Absolutely. However, a sucky workaround is better than a nonexistent workaround.

Also, I control the internal implementation of my classes, whereas proxying happens by other parties. As a library user, it must be incredibly confusing to have to deal with errors about access to private fields in a class you did not write.

This was one of the saddest PRs I have ever written https://github.com/LeaVerou/color.js/pull/306. It feels like such a huge step backwards. I’ve waited years for private fields to be supported everywhere and relished when they got there. I was among the first library authors to adopt them in library code, before a lot of tooling even parsed them properly (and some still don’t).

Sure, they were kind of annoying to use (you usually want protected, i.e. visible to subclasses, not actually private), but they were better than nothing. I was not joking in the first paragraph; I am literally grieving.

I may still use private fields on a case by case basis, where I cannot imagine objects being proxied being very useful, for example in web components. But from now on I will not reach to them without thought, like I have been for the past couple of years.

Categories
Articles

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

Reading Time: 2 minutes

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:

She went on to make the point that if a design looks like buttons, it should act like buttons, otherwise there are mismatched expectations and poor UX for AT users:

In case you were wondering if state would be equally noticeable with aria-pressed and buttons, it is:

And some advice on grouping:

In theory doing this in Shadow DOM and/or using ElementInternals implicit roles should be fine, though in practice we’ve had some trouble with that.

Today I posted my attempt to implement what we’ve discussed in a <button-group> component, which restarted the discussion.

Its implementation is right here if you want to improve it further! And make sure to check out the actual Twitter thread, as there is a lot of good stuff I couldn’t include in this!

Edit: Léonie wrote a blog post too, Perceived affordances and the functionality mismatch. It’s a great read.

Categories
Articles Original Tutorials

Custom properties with defaults: 3+1 strategies

Reading Time: 4 minutes

When developing customizable components, one often wants to expose various parameters of the styling as custom properties, and form a sort of CSS API. This is still underutlized, but there are libraries, e.g. Shoelace, that already list custom properties alongside other parts of each component’s API (even CSS parts!).

Note: I’m using “component” here broadly, as any reusable chunk of HTML/CSS/JS, not necessarily a web component or framework component. What we are going to discuss applies to reusable chunks of HTML just as much as it does to “proper” web components.

Let’s suppose we are designing a certain button styling, that looks like this:

Categories
Articles Original Tutorials

Inherit ancestor font-size, for fun and profit

Reading Time: 7 minutes

If you’ve been writing CSS for any length of time, you’re probably familiar with the em unit, and possibly the other type-relative units. We are going to refer to em for the rest of this post, but anything described works for all type-relative units.

As you well know, em resolves to the current font size on all properties except font-size, where it resolves to the parent font size. It can be quite useful for making scalable components that adapt to their context size.

However, I have often come across cases where you actually need to “circumvent” one level of this. Either you need to set font-size to the grandparent font size instead of the parent one, or you need to set other properties to the parent font size, not the current one.

Categories
Articles Original Tutorials

Dark mode in 5 minutes, with inverted lightness variables

Reading Time: 7 minutes

By now, you probably know that you can use custom properties for individual color components, to avoid repeating the same color coordinates multiple times throughout your theme. You may even know that you can use the same variable for multiple components, e.g. HSL hue and lightness:

:root {
	--primary-hs: 250 30%;
}

h1 {
	color: hsl(var(--primary-hs) 30%);
}

article {
	background: hsl(var(--primary-hs) 90%);
}

article h2 {
	background: hsl(var(--primary-hs) 40%);
	color: white;
}

Here is a very simple page designed with this technque:

Unlike preprocessor variables, you could even locally override the variable, to have blocks with a different accent color:

:root {
	--primary-hs: 250 30%;
	--secondary-hs: 190 40%;
}

article {
	background: hsl(var(--primary-hs) 90%);
}

article.alt {
	--primary-hs: var(--secondary-hs);
}

This is all fine and dandy, until dark mode comes into play. The idea of using custom properties to make it easier to adapt a theme to dark mode is not new. However, in every article I have seen, the strategy suggested is to create a bunch of custom properties, one for each color, and override them in a media query.

This is a fine approach, and you’ll likely want to do that for at least part of your colors eventually. However, even in the most disciplined of designs, not every color is a CSS variable. You often have colors declared inline, especially grays (e.g. the footer color in our example). This means that adding a dark mode is taxing enough that you may put it off for later, especially on side projects.

The trick I’m going to show you will make anyone who knows enough about color cringe (sorry Chris!) but it does help you create a dark mode that works in minutes. It won’t be great, and you should eventually tweak it to create a proper dark mode (also dark mode is not just about swapping colors) but it’s better than nothing and can serve as a base.

Categories
Articles

Mass function overloading: why and how?

Reading Time: 4 minutes

One of the things I’ve been doing for the past few months (on and off—more off than on TBH) is rewriting Bliss to use ESM 1. Since Bliss v1 was not using a modular architecture at all, this introduced some interesting challenges.

Categories
Tutorials

Writable getters

Reading Time: 5 minutes
Setters removing themselves are reminiscent of Ouroboros, the serpent eating its own tail, an ancient symbol. Media credit

A pattern that has come up a few times in my code is the following: an object has a property which defaults to an expression based on its other properties unless it’s explicitly set, in which case it functions like a normal property. Essentially, the expression functions as a default value.

Categories
Articles

The case for Weak Dependencies in JS

Reading Time: 5 minutes

Earlier today, I was briefly entertaining the idea of writing a library to wrap and enhance querySelectorAll in certain ways. I thought I’d rather not introduce a Parsel dependency out of the box, but only use it to parse selectors properly when it’s available, and use more crude regex when it’s not (which would cover most use cases for what I wanted to do).

In the olden days, where every library introduced a global, I could just do:

if (window.Parsel) {
	let ast = Parsel.parse();
	// rewrite selector properly, with AST
}
else {
	// crude regex replace
}

However, with ESM, there doesn’t seem to be a way to detect whether a module is imported, without actually importing it yourself.

I tweeted about this…

Categories
Articles

Developer priorities throughout their career

Reading Time: 3 minutes

I made this chart in the amazing Excalidraw about two weeks ago:

It only took me 10 minutes! Shortly after, my laptop broke down into repeated kernel panics, and it spent about 10 days in service (I was in a remote place when it broke, so it took some time to get it to service). Yesterday, I was finally reunited with it, turned it on, launched Chrome, and saw it again. It gave me a smile, and I realized I never got to post it, so I tweeted this:

The tweet kinda blew up! It seems many, many developers identify with it. A few also disagreed with it, especially with the “Does it actually work?” line. So I figured I should write a bit about the rationale behind it. I originally wrote it in a tweet, but then I realized I should probably post it in a less transient medium, that is more well suited to longer text.

Categories
Articles Original

The Cicada Principle, revisited with CSS variables

Reading Time: 4 minutes

Many of today’s web crafters were not writing CSS at the time Alex Walker’s landmark article The Cicada Principle and Why it Matters to Web Designers was published in 2011. Last I heard of it was in 2016, when it was used in conjunction with blend modes to pseudo-randomize backgrounds even further.

So what is the Cicada Principle and how does it relate to web design in a nutshell? It boils down to: when using repeating elements (tiled backgrounds, different effects on multiple elements etc), using prime numbers for the size of the repeating unit maximizes the appearance of organic randomness. Note that this only works when the parameters you set are independent.

When I recently redesigned my blog, I ended up using a variation of the Cicada principle to pseudo-randomize the angles of code snippets. I didn’t think much of it until I saw this tweet: