It is a solid solution for blogs and apps with a distinct document feel, but for anything beyond that I found it too limiting and brittle. Back to components and Tailwind.
Found "<span class=..." — What?
Read the page.
Footer : "I only got 99% of the way there. I use 11ty’s syntax highlighting plugin, which uses classes for styling."
From an outside perspective, it is perplexing to see the constant back and forth webdevs do between making website more complex and rediscovering the simpler first principles
I feel there's a mismatch between creating novel "semantic" elements, and then customising them in the markup, rather than the contextual approach (nesting, rich selectors). The mismatch is that the new elements still apply a "what" approach, but the attributes used for customisation apply a "how" approach and leave it in the mark-up. It's still like `<p class="red" />` rather than `main p { background-color: red; }`.
I get that there's a trade-off between purity and code that's nice to work with, and I think you've hit a very readable, appealing and creative balance.
Tailwind actually complicates a lot more things, when you have to specify variants for example, there you go installing tw-variants, writing Javascript just so you can get different sorts of buttons.
This is fine for larger component libraries like shadcn-ui, but for simplicity, I'd pick up pure CSS for something like button .error; and button .secondary.
(yes I know you can just @apply whatever you want inside those blocks, but what's the benefit of tailwind then?)
Tailwind can quickly escalate into very very long class name chains, daisyui cuts that down by a ton. Yea its yet another dependency but definitely worth a look. Phoenix adopted it as default too.
That's around 2% of the size of the single page of that article, it absolutely is a trivial amount, especially when it complexifies so much the maintenance or addition of the website.
It also brings back memory of 2000s internet, but merged into Today's design standards. I assume this was intentional.
If you read the article you'll see what they're talking about. It's not "CSS is too limiting" it's "CSS only applied to elements is too limiting".
Sep 14, 2025
In my recent post, “There’s no such thing as a CSS reset”, I wrote this:
Think of elements like components, but ones that come packed in the browser. Custom elements, without the “custom” part. You can just like, use them.
The line continued to rattle around in my head, and a few weeks later when I was digging into some cleanup work I came to an uncomfortable realization; I wasn’t really taking my own advice. Sure, I was setting some default element styles, but I was leaving a lot on the table. I felt attacked. Called out even. Present me, positively roasted by past me. There was only one possible solution; refactor my website.
I like to apply severe constraints in designing and building this site – I think constraints lead to interesting, creative solutions – and it was no different this time around. Instead of relying on built in elements a bit more, I decided to banish classes from my website completely. I haven’t used a class-free approach since the CSS Zen Garden days, and wanted to se how it felt with modern HTML and CSS.
CSS for the site was structured around 3 cascade layers; base
, components
, and utilities
. Everything in base
was already tag selectors, so the task at hand was to change my approach for components, and eliminate utilities completely.
Step 1? Mitigation. There was plenty of code that could have been styled defaults but wasn’t, so I gave all my markup a thorough review, increasing use of semantic elements, extracting common patterns in the form of new element defaults, and making more use of contextual element styling. By contextual styling, I mean going from something like this:
.header-primary {
margin-block: clamp(var(--size-sm), 4vw, var(--size-lg)) var(--size-flex);
}
To something like this:
body {
background-color: var(--color-sheet);
& > header {
margin-block: clamp(var(--size-sm), 4vw, var(--size-lg)) var(--size-flex);
}
}
It was a good start, and modern features like nesting, :where()
, and :has()
made this feel better that it did 20 years ago, but I took things way too far with contextual styles. Taken to the extreme, you end up with overloaded selector definitions and progressively more esoteric selector patterns. I knew I was down the rabbit hole when I did something like this:
li {
&:has( > a + p) {
padding-block: var(--size-lg);
border-block-end: var(--border-default);
text-wrap: balance;
& > a {
font-size: var(--font-xxl);
}
& > p {
margin-block: var(--size-sm);
}
}
}
I still needed a “real” solution for components, and a way to manage variants.
I had an inkling of a solution, which is to leverage patterns from custom elements and web components, sans js. By virtue of their progressively enhanced nature, custom tag names and custom attributes are 100% valid HTML, javascript or no. That inkling turned into fervent belief after reading Keith Cirkel’s excellent post “CSS classes considered harmful”.
Revisiting the example above, now we’ve got a pattern like this:
note-pad {
padding-block: var(--size-lg);
border-block-end: var(--border-default);
text-wrap: balance;
& a {
font-size: var(--font-xxl);
}
& p {
margin-block: var(--size-sm);
}
}
Custom attributes become a go-to for handling former BEM modifiers, but instead of relying on stylistic writing convention to fake a key-value pair, you get an actual key-value pair.
random-pattern {
& [shape-type="1"] {
border: 0.1rem solid var(--color-sheet);
background-color: var(--color-sheet);
filter: url("#noise1");
}
& [shape-type="2"] {
background: var(--pattern-lines-horizontal);
background-size: var(--pattern-scale);
}
}
Now, you can use data-whatever
for attributes, but really, any two dash-separated words are safe. Personally, I think dropping the data
prefix feels better and allows for richer semantics.
You can argue that both of these techniques are re-inventing classes in various ways. Kind of! You can use custom element names in lieu of semantic tags, just like you can slap a class on a div. But these techniques, particularly with how you can seamlessly enhance to true custom elements or web components, feels like a coherent end-to-end system in a way that class-based approaches don’t. It’s tags and attributes, all the way down.
On the plus side, the user outcomes are decidedly positive; I removed a non-trivial amount of CSS (now about ~5KB of CSS over the wire for the entire site), and accessibility is without question better due to having to paid much closer attention to markup. Also, just look at that markup. So clean. So shiny.
On the flipside, this feels like an approach that simply asks more of authors. It requires more careful planning compared to pure component approaches; you can’t think of things in purely isolated terms. All to say, I’m very happy to ship this on my personal website, I’d be less likely to advocate for this approach on a large project with varied levels of frontend knowledge.
There’s a variation here that’s more encapsulated (use custom tag names with abandon), but that pulls on what feels like an unresolved thread; replacing a semantic element with a custom tag name that has no semantic value feels bad, and adding extra wrappers around everything also feels bad.
All to say, I’m not quite ready to say that this is The One True Way I’ll build all sites from now on, but I also can’t help but feel like I’ve crossed some kind of threshold. I used to think classes were fine. Now I’m not so sure. I don’t know exactly where it’ll lead yet, but this feels like one of those exercises that’ll have a lasting influence on my work.
A mea culpa; I only got 99% of the way there. I use 11ty’s syntax highlighting plugin, which uses classes for styling. I gave syntax-highlight a hard look, but I don’t love the idea of introducing client-side js where none need exist, and the authoring experience would be a step back, so I begrudgingly left it alone for now.