2021 in review: less JavaScript, and CSS variables that survive the build
A quiet year, tool-wise, and a good one. Two changes: I replaced a lot of component-framework code with Alpine.js, and I started using CSS custom properties as an actual feature rather than as a preprocessor variable with different syntax.
Alpine.js instead of a framework
Half the "apps" I had built were a server-rendered page with a dropdown, a modal and a filter. That does not need a build-time component framework, a router and a state library. It needs about twenty lines.
<div x-data="{ open: false }">
<button @click="open = !open" :aria-expanded="open">Filters</button>
<div x-show="open" x-collapse>
...
</div>
</div>
State in the markup, next to the thing it controls. For a filter panel that is not a step backwards from a component, it is the same information in one place instead of three files.
Where I do not do this: anything with genuinely shared state across the page, or a form complex enough that validation has its own logic. That still gets a real component framework. The distinction I use now is whether two separate parts of the page need to agree about something.
Custom properties are not Less variables
I wrote a post here in 2014 recommending Less, and I left an update on it this year. The replacement is CSS custom properties, and treating them as a like-for-like swap wastes the interesting part.
A preprocessor variable is resolved at build time. By the time the browser sees the CSS it is gone:
@brand compiles to #c2410c and there is no @brand at runtime.
A custom property is a real value that lives in the cascade, inherits, and can change after the page has loaded. Everything below follows from that.
Theming without a second stylesheet:
:root {
--paper: #ffffff;
--ink: #14171c;
}
@media (prefers-color-scheme: dark) {
:root {
--paper: #0f1216;
--ink: #e8eaee;
}
}
Scoped overrides, so variants stop being a new class per property:
.card { --pad: 1rem; padding: var(--pad); }
.card--roomy { --pad: 2rem; }
Values JavaScript can change without touching a class list:
document.documentElement.style.setProperty('--accent', userColour);
What you lose is colour maths. darken($brand, 10%) has no direct equivalent yet, so I define the
shades explicitly, which is more typing and better discipline. You also lose compile-time errors: a
typo in var(--brnd) silently falls back. Use an obnoxious fallback colour during development and
you will spot it in a second:
background: var(--brnd, hotpink);
The gotcha that cost me an hour: custom properties substitute as tokens, not as values.
:root { --w: 50; }
.thing { width: var(--w)px; } /* nonsense, "50px" is never formed */
.thing { width: calc(var(--w) * 1px); } /* this is what you meant */
And they do not work in media query conditions, because those are evaluated before the cascade. Breakpoints stay hard-coded, or stay in a preprocessor. Which is the honest summary of my year: I still have Sass in most projects, purely for breakpoint variables and two mixins. Everything else is plain CSS now, and shorter than the Sass that used to generate it.
Icons became SVG
The other quiet change. The icon font sets I was shipping in 2015 are gone from my projects this year, replaced by inline SVG and a sprite.
The reasons are all things I ignored for six years. An SVG is an element, so it can be given a
title or hidden with aria-hidden honestly, instead of being a character that a screen reader
tries to pronounce. It cannot fail to load and leave a square. It takes currentColor, so it
inherits from the CSS around it without a second class. And you ship the four icons you use rather
than a set of six hundred.
<svg class="icon" width="20" height="20" aria-hidden="true">
<use href="/icons.svg#search"></use>
</svg>
One sprite file, cached once, referenced by fragment. That is the whole technique, and it replaced four font formats per project.
In short
Replaced component-framework code with Alpine.js, moved from preprocessor variables to CSS custom properties, and swapped icon fonts for SVG.