61 comments

[ 6.8 ms ] story [ 111 ms ] thread
I've been writing CSS for well over a decade, and as much as I love it, there's one thing I've always been irked about. The language lacks an explicit if/else syntax because the authors felt that the presentation layer shouldn't be responsible for "behavior", which is how they think of logic. So what's funny here is that this article perfectly illustrates why the lack of an else/if is just pure silliness. There's "behavior" embedded everywhere in CSS, it's just all so implicit that it almost goes unnoticed.
I think you and the blog post make sense. If you present people with a outside-in view of building a website, I wager they would agree that form and presentation layers have a huge impact of behavior, expected or implicit.

With the inside-out view of HTML then CSS, it feels like that same stance gets muddied because of the technical aspects of the languages and their original intents.

What's an example of something you could do with an if/else, that's not possible today? I think this article demonstrates why an if-statement is not needed when you can declare rules of how styles are applied.
It’s not about functionality, it’s about being explicit and clear. Had the authors decided to include an if/else construct, it would have meant that none of those other concepts were even necessary. You could have bundled all of them together under one syntax. Much cleaner and easier to understand.
I think part of the problem was that selectors that looked inwards or outwards from an element was deemed too expensive to parse. It only looked at the current element or following adjacent ones (with + or ~). Now that it's deemed doable without noticeably hurting performance, :has/:not(:has()) basically comes around and allows you the inward/outward selector lookups, e.g. (`.container:has(input:checked) { border: 1px solid green }` ).

That said, I do agree it would've been nice if they had the foresight to use `:has(:checked)` (or some kind of :if() syntax), but it's all hindsight 20/20 and it was the wild west on the web back when a lot of these features were spec'd/implemented. Kinda wish there was a new css version every 10 years or so to fix the inconsistencies (which you would have to opt into).

What's an example that's not explicit or clear enough?

I think `p:empty { }` is a perfectly clear syntax. As the article says it's already a conditional statement - just imagine an invisible `if` in front of every selector or media query.

CSS is not a procedural language, so I think that if statements could be less clear about how rules are applied.

> just imagine an invisible `if` in front of every selector or media query.

That reminds me of how I'm supposed to imagine an invisible watashi or an invisible ga in Japanese sentences, which is also incredibly difficult for for non-native speakers to do well (at least, in the beginning, which most do not get far beyond) and causes all sorts of problems for translation engines.

So how about making the language explicit, clear, and hence easier for humans to reason about? All this all you have to do stuff reminds me of how adherents speak about C and Git. Thank God we're seeing viable replacements for C, and I have hope in a replacement for Git (jj or pijul? I pray). If something comes along that is less painful than CSS I'll be the first to jump and I won't be alone.

I think the way most people would want an if else to work in CSS is that you can say if this path exists then apply a list of rules that might not have anything to do with that path else some other list.

Let's suppose we have a match function that could be used like this

:root {

  --has-path: match(.modal .message .error);
}

and then if the following were allowed (or some variation were developed where this could be done)

.user:has(var(--has-path)) {color: red}

I think it might be nice. YMMV however.

on edit: thinking about it however while it might be nice if you could do queries like that, it does seem like it would be expensive (for browser performance) to implement.

The only missing part from your example is supporting custom properties in selectors, which can't really work because custom properties are scoped and cascade, like regular properties.

I'm not sure I totally understand the specifics of the example, but it's probably possible to get something close with the nested selectors spec being worked on now, and/or :is()

>The only missing part from your example is supporting custom properties in selectors

I figured it was implied some rules would have to be made which might be properties used in selectors are scoped to ancestors of element.

>I'm not sure I totally understand the specifics of the example

example is probably too fanciful but assuming you have a match of selector

.modal .message .error

then element with class user which is on a totally different part of the page - like

body

header

   ....stuff .user
 
/header

article

section

.modal .warning .error

then user has color red. Do I personally want this, not much, just considering the people who sometimes say they want if ( selector exists) then bunch of css.

another more likely usage for something like this (a match function that takes a selector) might be the various CSS logical operations things people do sometimes https://css-tricks.com/logical-operations-with-css-variables...

however, as I noted before I don't think it makes sense for css performance reasons, despite how interesting/amusing I might find the idea.

The biggest problem is that it would completely kill all of the precomputation style engines do to make CSS matching fast. When you style an element, you don't actually go through the steps of matching it against every rule; but if the selector could change at any time, you basically would have to, and performance would tank completely.

The other issue is that custom properties inherit, so now it's unclear what element you should fetch your var() from (you can't fetch it from the element you're styling, since you haven't computed its style yet). E.g.

.user:has(var(--has-path)) { --has-path: match(.something .else); }

>The biggest problem is that it would completely kill all of the precomputation style engines do to make CSS matching fast

which is why I specifically said it wouldn't work because of performance issues? This part here at the end >it does seem like it would be expensive (for browser performance) to implement.

>The other issue is that custom properties inherit

I'm not sure I see the problem with that actually -

If I set a value on :root and then override it on .foo then I thought it only got overridden for the descendants of .foo?

on edit: ah no of course you mean that if I set --base-color on .foo it is also available for .foo, but in that case I would think the rules would mean that use in selector was outside the scope of the element, that is to say scoped to the parent.

If I understand this right, you're saying: if my `.user` or `.something-else` has a `.modal .message .error`, then colour all the text in the `.user` red.

I think this is a mistake in system design. CSS doesn't know or care or work with what a user is, what a foo is, what a bar is or how any of those interact conceptually. CSS operates on the rendered output of decisions made at that level: it shouldn't make those decisions, nor encode knowledge of those decisions. That's what JS is for, and what it's better at.

I think it makes much more sense architecturally to have JS respond to whatever condition triggered the error state by updating some metadata on everything involved (User{}, Booking{}, Etc{}).

```

// pseudo JS uiEvents.subscribe('error message', () => {

  appState.User.hasBadData = true;
})

uiEvents.subscribe('error message', () => {

  appState.Booking.hasBadData = true;
})

function validateForm(fields) {

   fields.forEach(field => {

     if(field.hasError) {

        uiEvents.publish({some: "error message"});

     }

   }
}

// pseudo React/Vue/whatever in pseudo TypeScript

render(({user, booking}: TAppState) => (

  <>

   <div class={`user ${user.hasBadData? 'user--error' : ''}`}>/\* user icons and text */</div>

   <div class={`booking ${booking.hasBadData? 'booking--error' : ''}`}>

    /* booking stuff \*

   </div>

  <>
});

```

You could replace all the JS with server-side logic (which would probably take it closer to the "single source of truth" of users and booking anyway). Further, maybe you're making application with multiple communication modalities, and you want to play a sound or trigger speech when the user enters bad data, instead/as well as colouring everything red. CSS can't help you there. It's much better to keep the logic, state and behaviours out of the CSS and use it only to "dress up" the rendered markup, otherwise you make maintenance harder.

Cannot read inputted text from textarea/input/contenteditable div element into a var that is then conditioned - limited to explicit input (in)validation match.
CSS is reaching the point for me that it is becoming too complex. I'm perfectly fine with complex backends though - CSS though, there are now so many options and so many ways to do things. It must probably be me though.
It isn't just you.

I work on systems software - kernel, daemons, networking, debuggers, machine language disassembly - so I'm used to looking at things which other people say are "hard".

Nothing frustrates me more than writing a Stylus rule or figuring out how to place an element on my Jekyll theme. Every time I do this, my appreciation for modern well-made websites doubles.

CSS is so opaque and impossible, I wouldn't be a web developer for any money.

The hard part about working with CSS is that, beyond syntax, nothing is an error. No matter how far it is from doing what you intended to do it's always going to give you a layout with no hint how it's actually different from what you wanted beyond "that doesn't look right".
But is that very different from code with logic bugs despite correct syntax? The solution for code is unit tests. Maybe the css needs that also.
In code it's very easy to add your own errors/asserts which say "you dummy, you can't possibly fit these 3 things here the way you want" and then go modify your code so you can handle it the way you wanted. It's worth testing because when a test breaks you can modify it so the test passes.

In CSS you can't customize that latter half though, if the 3 things render wonky when they don't fit you just have to hope there is some way to tell CSS to render it the way you want. Sure, you can run a test that says it failed to create the layout you were hoping for... but you already knew that and test or no test your only hope is you can find some way to do it with the predefined options in CSS. And the fun part is sometimes there is no way! You may just have to compromise on how your layout works or push some logic to JS to change the styling so you can do something more akin to the normal code way (at the loss of performance and clarity).

That's simply because you haven't learned it properly. For example, I went through a course that taught CSS from the ground up [0] and I am now able to write any CSS I need to, and I can generally debug CSS issues a lot faster.

The problem is most devs, even on the frontend, learn CSS incidentally, not from first principles, so they get annoyed when they can't do stuff. I mean, yeah, if you don't learn something properly of course it won't work as you expect. It's the same as me not understanding Docker until I also learned it from the ground up.

[0] https://css-for-js.dev

It may be time for CSS to consolidate, factor some functionality, and deprecate some old stuff...

But I can't point any functionality that could go through that. Ok, there are a few functionalities that are not independent, but they are still better designed than any alternative I can think.

I'm staying with the opinion that a universal formatting language is inherently complex. And CSS is quite well fit to function.

One thing that would be interesting to see is how a complex webpage performs without the limitations CSS places to allow fast styling computation. E.g. in regards to this article there is no explicit "if" in CSS because allowing generic comparisons instead of specific ones related to elements and parent/children elements would break core assumptions styling engines get to use to handle CSS at lightning speed. At the same time I wonder if having all these limitations is part of why auto-generated sites have ridiculous amounts of elements, attributes, and styling to parse in the first place.
CSS doesn't have explicit "if" because it's a logical language, and thus every single thing you write is an "if".

The only things missing from it having universal logic are coming with the ":has" selector. So there isn't anything to argue about performance. The new ":has" is a performance killer, everybody knows it, nobody really cares because it's only a problem if you abuse it... and of course people will make a point of abusing it.

Interestingly, it is also getting predicate composition, with hierarchical selectors. Looks like the CSS people finally decided to turn it into a usable logic language. The auto generated sites have huge amounts of them because both auto generated code tends to be bad, and because it lacks composition. One of those is being solved.

There are definitely selectors that have more of a performance impact than others but they still don't allow you to break some general assumptions. E.g. taking :has() the addition is styling an element base on a relative selector anchored to that element but it doesn't allow for arbitrarily anchored selectors or matching things which could result in cycles. A lot of what browser engines did to make that work reasonably fast like here https://twitter.com/anttikoivisto/status/1473251189181591554 might not work efficiently with something less restrictive such as a true general purpose if().

I'm not familiar with what you're referring to with hierarchical selectors. CSS combinators have always let you select different types of decedents and :has() lets you select either way. Are you referring to Cascade Layers, or maybe something else?

The current syntax also makes it hard to characterize the class of predicates that can be expressed with CSS. Kind of hurts my sense of perfectionism, but I guess it wasn't their goal.

I assume CSS was made very restrictive on purpose so that it can be applied to the page elements and analyzed very efficiently.

Years ago I came to the conclusion that CSS should be treated as just a place to dump reusable styles, and all logic should be in the html/templating language. It's made my life significantly easier.
> Years ago I came to the conclusion that CSS should be treated as just a place to dump reusable styles, and all logic should be in the html/templating language.

Coding like this removes so much of the flexibility and power of CSS.

The reason we have media queries and now container queries and style queries is because HTML is for structure and CSS is for presentation and the state of things being presented.

If a user selects high contrast mode because they're sight impaired, that's a state issue that has to be handled by CSS; HTML can't help you here:

    @media screen and (forced-colors: active) {
    …
    …
  }
To anyone who's learned CSS properly sees this as "IF the user wants forced-colors mode (previously high-contrast mode), THEN execute the selectors between the braces."

This is certainly cleaner than an explicit IF/THEN construct for this particular feature.

I recently tried that and it simply doesn't work because even in the latest version CSS is unusable for anything remotely complex or without repeating code.

Gave up and am doing it like OP now. CSS MUST become a proper programming language (or at least non-turing-complete configuration language like dhall) to solve this.

> HTML is for structure and CSS is for presentation

These really arent decoupled.

I believe the reason there's no if/else is because CSS is supposed to be easy enough to interpret relatively quickly (in a finite amount of time). The moment you add if/else you'd up with a full programming language and the halting problem and an arbitrary amount of calculations before the system can finally know what to render.
I think that would require while/recursion? Afaik ifs are very quick to calculate.
So what's the issue here? Why is this not a problem for Javascript but for CSS?

In the worst case just put a 3sec limit on the evaluation and stop it if it doesn't complete in time, problem solved.

You solved the halting problem!
Great article. I am super excited for :has, I think it will open up a lot of doors to truly declaritive UI with vanilla CSS and HTML.

One more handy feature is data attributes. You can use them to introduce some state to your UI.

Here's a quick and dirty example of a menu for an RPG game.

  <ul id="menu" data-can="">
    <li id="menu-attack"><button>attack</button></li>
    <li id="menu-magic"><button>magic</button></li>
    <li id="menu-run"><button>run</button></li>
  </ul>
We can use data attribute selectors to hide menu buttons that the player can't use.

  #menu:not([data-can~=attack]) #menu-attack,
  #menu:not([data-can~=magic]) #menu-magic,
  #menu:not([data-can~=run]) #menu-run {
    display: none;
  }
Then all we need to update the menu UI is this single line of JS.

  document.getElementById("menu").dataset.can = getPossibleActions().join(" ");
You can see a rough example here: https://github.com/kawaiisolutions/tactics. It's very nice for prototyping.
Yes, I use data attributes all over! It lets me keep certain data and certain states in the DOM, and I can actually use querySelector to... you know, do a query based on data attribute values. I've met many devs who didn't know that CSS queries can actually string match within the values of attributes! (i.e. [attribute~="value"] and the other variations)

https://css-tricks.com/almanac/selectors/a/attribute/

You can already have states and this game can be implemented in CSS and HTML without a single line of JS, but it would be extremely ugly and hard to follow, IMO.
This article has interesting points; I learned a few CSS features. Yet the premise seem rather bland: of course CSS is "conditional"! No need for fancy media queries and whatnot, a basic selector is already a "condition". Styling different elements in different ways is the whole point of CSS.
At first I assumed this was a blast from the past article about having to feed special styles to older versions of IE using syntax that was invalid in other versions. Like using `_` or `*` characters in properties. I had to look up all those old tricks I used to know by heart, from the bad old days.
So useful; thanks to the author.

But a number of things are still Chrome-only, and even with that, only available in the latest version. Will have to wait before I can apply them.

Style queries are the only thing he talked about that's Chrome-only, and Chrome hasn't even shipped them yet. They're only available in Chrome Canary behind a flag.

Firefox is working on shipping :has() and Container Queries.

Safari supports everything besides style queries. Many of these, Safari shipped first. More importantly, these days, browsers often ship new CSS around the same time.

Plus you can always use Feature Queries (@supports) to conditionally test for support and apply different CSS depending on what's supported. That way users who haven't updated their software in years have a great experience, too.

So no, you don't have to wait to use this CSS. Just think through what you are doing. Learn how here: https://www.youtube.com/playlist?list=PLbSquHt1VCf1kpv9WRGMC...

could you use the :checked or :has selector for opening up mobile menus? i currently use JS for that and feel a bit guilty about it, yet i dont really know how to do it in pure CSS and hide the checkbox somehow
Be careful in this space, keep accessibility in mind when doing things like this. JS is meant for interactivity, don't feel guilty for using it for the right job.
Yes, this is what I do, with :checked and <details> and some sibling selector trickery instead of :has (basically, the checkbox is the last element, and <details> is an accessible browser-native way to toggle visibility without javascript).

I was able to make some complex hover menus and sidebar toggles work like this entirely without JavaScript. It's also accessible, to the user. The downside is that for the developer, the resulting code is not easy to read nor maintain.

I would link to it but it's in a private repository atm. One day I'll open source this behavior as its own package... one day...

chuck it in a codepen maybe?
You just hide the checkbox the way you normally would with CSS. Search for CSS Hamburger Menu - there are plenty of examples online. There's some other trick in there using target. It's been a while since I did it but once you get your head around it it's pretty simple.
an example of this is used for modals in daisyui -> https://daisyui.com/components/modal/#

the relevant style applied looks like this:

    .modal-open, .modal:target, .modal-toggle:checked+.modal {
        pointer-events: auto;
        visibility: visible;
        opacity: 1;
    }
Unfortunately, that lacks any of the semantics or properties a modal dialog should have, like role="dialog", managing focus when the dialog opens or closes, or making the page contents behind the modal inert (if the contents behind a modal aren't inert, it's not a modal).
You can also trigger the behaviour of Daisy components with CSS classes (and a sprinkle of JS). But yeah, their whole "CSS + :checked elements only" model really makes my brain hurt sometimes (e.g. the [Drawer](https://daisyui.com/components/drawer/) component... nothing is where it belongs).
You can, but shouldn't. Visually, it can work fine but misusing the semantics of a checkbox will be confusing to screen reader users and may make it difficult to operate for a voice control user.

Stick to using a button with an aria-expanded state to toggle. You can make the sibling menu contents visible based on that state.

button.menu[aria-expanded="false"] + .menu-list { display: none; } button.menu[aria-expanded="true"] + .menu-list { display: block; }

The reminds me a piece of CSS I wrote over the weekend for a hobby project. There is a game canvas where the playfield is 16:9 and you always see the whole field (For simplicities' sake think of Pong where it doesn't make sense to display only part of the field and changing the field dimensions to fit the screen dimensions alters gameplay too much). So the problem I came to was "I don't know what the dimensions of the screen are but I know I want this canvas to be 16:9 and fill the screen". Now there are two possibilities at this point 1) just do it in JS 2) find out what wizardry I need to learn today. I went for route #2 for the fun of it.

I quickly found and then ran into issues with aspect-ratio. Either the canvas could get smooshed at certain sizes if I set width: 100vw and max-height: 100vh (despite setting the aspect-ratio) or it wouldn't do anything but be the default 300x150 canvas if not specified. There may well be a way to fix this but I got bored and just as I was switching to write the JS I had couldn't help but think "this feels doable with calc() and min()/max()". So I popped open an Excel sheet, created some random widths and heights, and started messing around for formulas that worked but didn't rely on variables or explicit conditionals. What I came to was:

  --Canvas-Width:    min(100vw, calc(100vh * 16 / 9));
  --Canvas-Height:   min(100vh, calc(100vw * 9 / 16));
Which is basically using min() to be the "if we are vertically/horizontally constrained" conditional. At this point since I had the height and knew it was of 100vh I also had a lazy way out on vertically centering with another calc().

It was definitely fun to tinker to a solution but if anyone actually knows CSS and how to make the built in aspect-ratio work the same way on canvas I wouldn't mind hearing it :).

Today, you can use aspect-ratio (https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-rati...).

I had to once do something similar and I ran into 4 cases (which can be collapsed into 2 cases): add vertical borders if the window is very wide or a little narrow. Add horizontal borders if the window is a little wide or very narrow.

Per the above I ran into issues with Canvas and aspect-ratio, demo here https://jsfiddle.net/6m0v9n3d/1/ (try vertically smashing it like it was being played on an ultrawide and it won't hold the 16:9 aspect ratio anymore). Trying to size vertically results in the opposite issue. Trying not to size it to the viewport or parent results in the canvas staying the default size.
You are setting `width: 100vw;` which is why it doesn't work.

Try something like this: https://jsfiddle.net/pgaz7cmx/, which sets both, width and max-width.

Huzzah! I don't think I ever would have thought to set both width and max-width (to > 100 vw no less!). Makes sense now that I see it though, thanks!
max-width gets set to vh, not vw. I do dislike this solution because the ratio is encoded twice.

I’m sure there are other ways to achieve the same, some are perhaps more elegant?

What are CSS best practices for not feeling lost at sea in a single linear list of styles?

Should I group by behaviour type — put all the margins and padding in one place, all the font stuff in another? How does that jive with grouping in a different dimension: by element hierarchy. All the buttons stuff in one file. All the headings stuff in another.

This feels a very basic question so I hope someone will take pity on me and answer the question I was trying to ask.

> I published a book about debugging CSS

…and it looks amazing. From the sample chapters it’s clear the author knows their stuff. I wonder if they have considered creating a Python (or Clippy!) style linter for CSS that automatically offers advice?

> Should I group by behaviour type — put all the margins and padding in one place, all the font stuff in another? How does that jive with grouping in a different dimension: by element hierarchy. All the buttons stuff in one file. All the headings stuff in another.

I've been where you are.

All I can say is Inverted Triangle CSS (ITCSS) [1] solves the issues you're talking about. It's so obvious once you get it—it's one of those "why didn't I think of this" kind of thing. You write your code in specificity order. It sounds odd at first but it makes total sense and eliminates so many of the "issues" people complain about. It makes the cascade your friend and not your foe.

[1]: https://blog.codeminer42.com/how-to-organize-your-styles-wit...