td204

Web development, one post per year since 2002

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.

The year in commits

Commits by month in 2021: 1156 in total, peaking at 317 in Jun.
Jan 2021: 20 commits J Feb 2021: 96 commits F Mar 2021: 84 commits M Apr 2021: 54 commits A May 2021: 58 commits M Jun 2021: 317 commits 317 J Jul 2021: 141 commits J Aug 2021: 80 commits A Sep 2021: 23 commits S Oct 2021: 130 commits O Nov 2021: 107 commits N Dec 2021: 46 commits D