Chris’ Corner: More Surprising Powers of CSS

Kilian Valkhof has a good one this week, You don’t need JavaScript for that, as part of the HTMLHell yearly Advent Calendar (of blog posts). He opens with the rule of least power:

Choose the least powerful language suitable for a given purpose.

That’s fun for us CSS nerds, because CSS is a pretty low language when it comes to building websites. (It doesn’t mean you should try to write an accordion component in Assembly.) It means, as he puts it:

On the web this means preferring HTML over CSS, and then CSS over JS.

If you’re at the JS level already, it means preferring JS over a JS framework or meta language. It’s not that those things aren’t valuable (Heck, one of the purposes of CodePen is making using language abstractions and library easier) it’s that, well, it’s just a better idea to go lower level. There is less to download, less to break, higher chances of the browser optimizing it, higher chances it will be accessible, higher chances it will last over time. That stuff matters.

Killian opens with a custom “toggle” component, and really, component is probably too strong a word. It’s just a styled HTML checkbox. It’s not even all that much CSS, and no JS is used at all, to make this:

While I was reading, where I thought Killian was going was using an <input type="checkbox"> actually turn on and off features on a website. That’s the kind of thing that feels like definite-JavaScript territory, and yet, because of the classic “Checkbox Hack” (e.g. using the :checked selector and selector combinators) we actually can control a lot of a website with just a checkbox.

The ability to do that, control a website with a checkbox, has increased dramatically now that we have :has() in CSS. For instance:

<body>
  ... literally anywhere deep in the bowels of the DOM ...
  <input class="feature" type="checkbox">

We don’t have to worry about where in the DOM this checkbox is anymore, we can style anything there like this now:

body:has([.feature:checked]) .literally-anything {
  /* do styles */
}

We can start the styling choices way up at the body level with :has(), looking for that checkbox (essentially a toggle), and style anything on the page on if it is checked or not. That feels extraordinarily powerful to me.


Speaking of the mental crossover between CSS and JavaScript, Yaphi Berhanu’s Write Better CSS By Borrowing Ideas From JavaScript Functions was interesting. CSS doesn’t actually have functions (but just wait for it!) but that doesn’t mean we can’t consider how our thinking about the code can relate. I liked the bit about considering too many parameters vs not enough parameters. Yaphi connects that idea too too many selectors with too many repeat declarations, and yeah I can get behind that.

But where my mind goes is taking that lesson and trying to apply it to Custom Properties. I have seen (and written!) CSS that just takes Custom Property usage just too far — to the point where the code feels harder to reason about and maintain, not easier.

Like if you were going to make a variable for an animation…

.do-animation {
  animation: 3s ease-in 1s infinite reverse both running slidein;
}

You could decide that the right way to make this controllable and re-suable is something like:

:root {
  --animation-duration: 3s;
  --animation-easing: ease-in;
  --animation-delay: 1s;
  --animation-direction: reverse;
  --animation-fill-mode: both;
  --animation-play-state: running;
  --animation-name: slidein;
}

.do-animation {
  animation: var(--animation-duration) var(--animation-easing) var(--animation-delay) var(--animation-direction) var(--animation-fill-mode) var(--animation-play-state) var(--animation-name);
}

Maybe you love that? But I think it’s already going too far. And it could easily go further, since you could have another whole set of variables that set default fallbacks, for example.

It depends on what you want to do, but if your goal is simply “reusability” then doing like…

:root {
  --brandAnimation: 3s ease-in 1s infinite reverse both running slidein;
}

And then using it when you needed it is closer to baby bear’s porridge.


Little reminder: don’t sleep on View Transitions.

We won’t know what Interop 2024 will do until January, but based on the amount of upvotes from the proposals, I think it stands a good chance.

Jeremy Keith has a well measured take in Add view transitions to your website. It’s got links to good resources and examples on using it, like Tyler’s Gaw’s great post. It ends with an update and warning about how maybe it’s not such a great idea because it may “poison the feature with legacy content”. Meaning if too many people do this, the powers that be will be hesitant to change anything, even for the better. I agree that would suck if such a choice is made, as I do think there is room for improvement (e.g. transition groups). I dunno though, I think because this stuff is literally behind a feature flag, that’s enough of an at your own risk warning. If they want to change something that breaks any code I’ve shipped, I’m cool with that. I know the stakes.

I think we’ll start seeing more interesting examples and experiences soon. And! Gotchas! Like Nic Chan’s View transitions and stacking context: Why does my CSS View Transition ignore z-index? It is confusing. It’s like the elements kinda stop being elements when that are being transitioned. They look like elements, but they are really temporary rasterized ghosts. You can still select them with special pseudo element selectors though, so you should be able to get done what you need.


There is this inherit complexity with doing the whole Dark Mode / Light Mode thing on websites. There is both a system-wide preference that you can choose to honor, and that’s a good idea. And it’s likely that a site implementing this also offers a UI toggle to set the theme. So that’s two separate bits of preference you need to deal with, and the code likely handles them separately.

Christopher Kirk-Nielsen’s A Future of Themes with CSS Container Style Queries gives us a good taste of this. Let’s jump right to code:

/* No theme has been set, or override set to light mode */
html:where(:not([data-theme])),
:root[data-theme=light] {
  --color: black;
  --background: antiquewhite;
  /* … and all your other "variables" */
}

/* Apply dark mode if user preferences call for it, and if the user hasn't selected a theme override */
@media (prefers-color-scheme: dark) {
  html:where(:not([data-theme])) {
    --color: ghostwhite;
    --background: midnightblue;
    /* … and all your other "variables" */
  }
}

/* Explicitly set the properties for the selected theme */
:root[data-theme=dark] {
  --color: ghostwhite;
  --background: midnightblue;
  /* … and all your other "variables" */
}

If you read though that I think you’ll see, you need @media to deal with system preferences, then the HTML attributes to deal with a direct-set preference, and one needs to overlap the other.

Enter Style Queries. What they really are is: “when an element has a certain custom property and value, also do this other stuff.” Christopher’s “future approach” using them is more code, but I’d agree that’s more clean and readable.

/* Optionally, we can define the theme variable */
@property --theme {
  syntax: '<custom-ident>'; /* We could list all the themes separated by a pipe character but this will do! */
  inherits: true;
  initial-value: light;
}

/* Assign the --theme property accordingly */
html:where(:not([data-theme])),
:root[data-theme=light] {
  --theme: light;
}

@media (prefers-color-scheme: dark) {
  html:where(:not([data-theme])) {
    --theme: dark;
  }
}

:root[data-theme=dark] {
  --theme: dark;
}

/* Then assign the custom properties based on the active theme */
@container style(--theme: light) {
  body {
    --color: black;
    --background: antiquewhite;
    /* … and all your other "variables" */
  }
}

@container style(--theme: dark) {
  body {
    --color: ghostwhite;
    --background: midnightblue;
    /* … and all your other "variables" */
  }
}

Check out Christopher’s article though, there is a lot more to go over.


Let’s end with a little good ol’ fashioned CSS enthusiasm.

For many, many years, creating high-quality websites largely meant that designers had to fight what felt like an uphill battle to somehow make their ideas work in browsers. At the same time, sentences like “I’m really sorry, but this solution you designed just can’t be done with CSS” […]

This has now changed to the opposite.

Want to emulate and confidently design a layout that leverages the potential of CSS Grid in any of the major design tools like Figma, Adobe XD, or Sketch? Not possible. 

Matthias Ott, The New CSS

And:

I still keep Sass around, as well as PostCSS, for things that CSS just wont ever be able to do (nor should it), but they’re fading into the background, rather than being at the forefront of my mind when writing styles. And for small, simple projects, I don’t use them at all. Just pure CSS. I haven’t found it lacking.

Jeff Sandberg, CSS is fun again