Hacker News new | past | comments | ask | show | jobs | submit login
My favourite 3 lines of CSS (andy-bell.co.uk)
121 points by chenster on Feb 21, 2023 | hide | past | favorite | 83 comments



I am increasingly worried about the “smart” CSS solutions. The spec is already huge, the selectors are hard to read and google, the interplay between various features is devilishly complex. And the CSS community seems to be very fond of these smart hacks where people in the know say “Oh, that’s just Foo’s variation on Bar’s flex owl hammer” and you are left to study the CSS lore for hours to decrypt the two or three letters. I just wish we would value simplicity over cleverness.


CSS is one of the languages that everyone shits on because they never bothered to learn it properly. When you learned your programming language of choice you probably read a formal tutorial, or followed a full fledged course. Well once you spend some time learning CSS, it becomes incredibly easy to do almost everything you want.

If you know your basic CSS selectors `.stack > * + *` is very easy to understand. I didn't know `* - *` was sometimes referred to as "lobotomized owl selector" but it doesn't matter, `>`, `*` and `+` are all basic selectors that everyone working with CSS should know from the top of their head. None of these are "clever". If you can't read `.stack > * + *`, you don't know CSS selectors. I'd even argue if you don't use mostly `>` instead of ` ` (space selector) your stylesheets are probably unmaintainable.

CSS hacks were a shit show 10 years ago because of browser compatibility, but the thing presented here is not a hack, it's just a useful rule.


> I am increasingly worried about the “smart” CSS solutions.

I'm not worried.

> The spec is already huge, the selectors are hard to read and google, the interplay between various features is devilishly complex.

Actually things are getting less complex due the fact we don't need to use all of the hacks, work-arounds and polyfills that were once just what pretty much every web developer did not that long ago. And of course, the original sin of the misuse of HTML tables for layout.

Sure, it wasn't fun to deal with new specs coming out while trying to maintain existing code and attempting to understand when (or if) we can use the new stuff. Sometimes is was like trying to fly a plane while the plane is being retrofitted with new features.

The irony: many of the techniques described in the article is how we should have been doing things for along time. Hayden's article [1], which is the basis for this, was published nearly 10 years ago—these concepts aren't new.

[1]: https://alistapart.com/article/axiomatic-css-and-lobotomized...


> And of course, the original sin of the misuse of HTML tables for layout.

Hardly a sin - there were no alternatives at the time (1996 - 1998) if you wanted a tabular or grid layout.


Sure, but developers continued to use tables long after there were alternatives.

The first edition of Zeldman’s seminal book Designing with Web Standards [1] was released in 2003; two additional editions were released in 2007 and 2009.

[1]: https://en.wikipedia.org/wiki/Designing_with_Web_Standards


Maybe I'm just old as shit but this is nothing compared to some of the selector hacks we had to do back in the IE6/IE7 support era.

This isn't to say those hacks were good, mind you - just that their existence was never really a problem insofar as ability to understand them if you dealt with it once.


The bit about googling is correct. But I posed it to ChaTGPT and it explained it immediately.

Having said that, I also knew what it did immediately upon reading it, and I am not a CSS expert. One you know a little bit about selectors, then you should be able to decode this one rather easily.


It will explain anything immediately. Correctly, not necessarily. Did you check the explanation holds water? Seems like it's back to googling anyhow.


Maybe save us all the trouble and either copy-and-paste the robot's explanation, or share your own.


Asking ChatGPT to write html / css / tailwind is my favorite application so far.

I still can’t remember how to horizontally and vertically center an element but CGPT sure can.


I think CSS and the domain of laying out things in 2d (and responding to state changes) with code is actually inherently complex, so which ever layout system you use is going to have a steep learning curve.

Many of the newer CSS features prevent you using JavaScript to achieve the same thing, which is better as it results in better performance and easier to understand code.

I would rather have newer standardised selectors than see the same pattern re-implemented with JS differently in every codebase.


Better a single smart and simple CSS one-liner like this accompanied by an article link in the comment than a gazillion repeated CSS classes and exclusion rules.


I had the exact same sentiment when the arrow function was added to JS. My mind has not been changed in the years since. Clear is always better than clever, arrow functions are not clear (despite the desperate cries to the contrary by complexity loving programmers).


Of all the JS features you are picking the arrow function? Is it the syntax, or the this binding thing that works differently than function expressions / statements you are complaining about?

Because the syntax looks somewhat like the function notation in math, and a variation of it is also present in many programming languages, often functional, but also Java since version 8. It's quite widespread and not very bizarre / specific to JS.

You'd have mentioned tagged templates [1], I'd have to bow.

[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...


The syntax. I don’t see how prior use in mathematics or other languages has any bearing. JavaScript could have led the way by using a clear syntax instead of perpetuating complexity.


> how prior use in mathematics or other languages has any bearing

It counts, because it makes it familiar to many people.

Had JS introduced this syntax without it being anywhere else I would be completely with you: it would just be weird and unnecessarily complicated.

I find this syntax very clear too (though I still prefer function () { ... } for most functions). It has the merit to make some code using small functions, often somewhat functional, lighter to read. But this is a matter of taste, of course.


So that's how test.each works in jest!! Catharsis!


Ah, so it's actually used somewhere, and in a widespread tool above all? :-)


Like some sort of perverse Andy and Bill's law, the more powerful CSS selectors we have, the sloppier document structures get.


They're not that complex if you understand CSS

And let's face it… to understand the example code you only need understand what > + * do in CSS

Using intrinsic sizing, and the sorts of approaches Andy Bell suggests reduces the volume of CSS enormously and can often get rid of media queries and huge numbers of selectors that get repeated across viewport sizes

I'd really encourage you to watch this talk by Andy https://www.youtube.com/watch?v=5uhIiI9Ld5M


It is one of those things as an outsider you think that something is overly complex, and then you learn it, and understand that it's complex because the issue it is tackling is dynamic which often requires a level of complexity to apply over.

I had the same thought with Regex before learning it. Now I can read it like css selectors, and you'll notice that the complex patterns are that way because they have to cover dynamic situations (using ranges, lookbehind/lookahead, etc).


The lines in question:

    .stack > * + * {
      margin-block-start: 1.5rem;
    }
I got it immediately because I think I've been doing something similar for a while. I identified the problem it solves. I understand that if you haven't encountered the situation, it's probably hard to decipher.

It reads like: apply a top margin to all direct children that are not the first one.

I write it like this:

    .stack > :not(:first-child) {
      margin-top: 1.5rem;
    }
I probably should start using -block-start. I'll keep my selector though I think.

I didn't know about the default value parameter of --var, I'll probably use it too.


I write like yours more often and avoid using * selector as much as possible in my CSS. Using :not(:first-child) is more obvious for me and my co-workers who will read that CSS selector.


> avoid using * selector as much as possible in my CSS

So do I, I am somehow wired to think "* is going to be very bad for performance because now the rendering engine will need to apply this bunch of rules to any and every element on earth, which might not be only overkill, but which also might have undesirable side effect."

It might not really apply here but the feeling is present.


>The difference between fastest and slowest is only 43ms (0.043 seconds), and I saw around this difference between rendering the same css twice. I think it is best to go for whatever is most readable for you. Personally I find :first-child the most understandable at a glance.

https://www.reddit.com/r/web_design/comments/5u6e7b/comment/...


43ms is quite a slowdown isn't it? Handful of joules at least.


Actually, the only two comparable / equivalent options exposed in this page related to this discussion, ul > * + * vs li:not(:first-child), are 50.8ms vs 40.9ms (respectively) for 10 page loads. The browser is not named, so I would guess Chrome. I'm not convinced the difference is not proven significant, we need more data points than that to know for sure, but it seems insignificant. So yes, readability should be the determining factor here.

Other browsers might see different results, especially Firefox which has a very fast / efficient CSS engine.

ps: a difference of 43 ms for one page load would be huge indeed. By comparison, a frame in a 30 fps video is 33 ms.


:not applies to almost every element, so it's not any better.


very true indeed!


I haven't done CSS in about 6 years. So I don't understand why you'd ever use OP's version over yours?


For * + * versus :not(:first-child), I guess it's a matter of style. Maybe they are a bit different in the specificity of the rule, I don't know.

margin-block-* and margin-inline-* (versus margin-{top,right,bottom,left}) are more generic and take into account the writing-mode, direction, and text-orientation.

https://developer.mozilla.org/en-US/docs/Web/CSS/margin-inli...

Notably, for instance, if you want to support both rtl and ltr languages, margin-inline-start (instead of margin-left) will behave correctly and put the margin at the correct side.

If you ask me, it's a step towards specifying/conveying the intent and not hard-coding the look.


Specificity is the difference:

* + * is 0,0,0.

:not(:first-child) is 0,1,0.

:where(:not(:first-child)) would take it back to 0,0,0.


I think you should keep writing it the way you are, I found it a lot clearer.


We get so much power from just setting global scoped styles like this it's so strange to me that more of these patterns aren't common or included in minimal css frameworks.

With all the recent criticism of tailwind, css-in-js, and other bloated frameworks I truly feel like 90% of projects could get away with a handful of sensible global styles or "classless css" and then aggressive use of components with scoped styles. You hardly ever need anything else


This. So much this. I wish I could make my colleagues understand.


That requires the program itself to be well designed, without exceptions everywhere.


I think exceptions are fine and that is what you could use inline or scopes styles for. Exceptions everywhere is problematic but having this sort of pattern could encourage better design


It’s amazing that CSS has such advanced features that can do these things, but at the same time if you actually use them extensively and then I inherit your code, you kind of deserve a knifing.


Worst thing is the comment (if there's any) is probably something like

  /* lobotomized owl typography*/


Well at least you can google it… But it certainly doesn’t help. I keep my css specific to each class and try to be as explicit as possible to avoid any weird consequences.


That’s kind of the opposite problem that the article mentions though — if every component has its own specific CSS, then you don’t really have CSS, you just have SS. You miss out on the cascading.

I develop things at work similarly to you. JSS where every component has its own style, but you have a common `theme` file so that you can reuse the common styles via a library.

But I have a certain romantic feeling about… what if we could do 90%+ of our styling using these global-per-page rules, and overriding when necessary. The simplicity is really appealing


For me, your approach makes the CSS harder to maintain. Pages have loads of repetition and patterns. I’d hope to find a good balance between DRYing up patterns that benefit from it, and leaving space for special cases. I find code patterns like the owl/.flow shown in the OP help greatly with that.

Your approach, redefining all the properties per class, echoes avoiding using parent+subclass relationships in OO style code. Sometimes you benefit from the generalisation of a class hierarchy, sometimes you don’t. A hard rule in either direction gets in everyone's way.


The 'C' in CSS was a mistake. Consider that specificity is even a thing, and then consider that the ordering of your declarative CSS code matters. IDK, it does seem necessary to make the language workable, but there are far too many stylesheets out there with specificity hacks and !important.


It's almost like you're saying: because people use CSS without learning it or the problem domain it tries to solve, CSS is wrong.

The "C" is an attempt to make it easy to have a mixture of generic rules that apply across the whole page/site, and specific rules for alternatives and edge cases. CSS and particularly the C make it really easy to say:

button { border-radius: 20%; background-colour: teal; color: white; }

button.cta { background-colour: blue; colour: yellow; }

What's wrong with that? How would you do it otherwise? In a way that generalises to any HTML or XML element? Without changing HTML or XML? It's essentially a syntactic formulation of multiple inheritance. I accept that multiple inheritance is frowned upon and difficult, but it's not without precedent or practicality.


In response to "How would you do it otherwise?", like I said, it kind of seems like the language is just what it has to be in order to be workable. In other words, it seems like it designed itself to some extent. And you're right, CSS has practicality. But with almost any pre-built solution, those selectors would look something like `button.cta.xl-2-large:not(:is(.container > .button))`. At that level of complexity, which is not uncommon, you're better off just coming up with a unique ID for every situation and styling things by ID rather than trying to make them "semantic" with the selectors. That's the other problem is the mixture of selectors trying to be both semantic but also their increased expressivity changing the specificity. So if you change a selector, you might need to reorder your CSS or change a bunch of other selectors to match, which becomes untenable the more CSS you have.


I detect a pungent "code smell", and probably a design bug via your example selector.

Does the designer really want such a specific edge case to an edge case? Can they explain or justify it according to the design patterns they laid down? Why are we using such a general ".container" to specify a ".button", and whilst we're at it why do we have a "button" AND a ".button". Our codebase was going off the rails long before we reached this (admittedly scary-looking) selector. There was a better way to do what we're doing, several iterations ago, and people who cared about the ux+information design and the CSS should have caught it sooner. Go back to them and fix the problem there rather than let this horrible selector pass code review.

We wouldn't stand for this in the TS/JS codebase. Why do we put up with it in the CSS?

To be honest, I was never on board with the "classes everywhere" approach. IDs are fine when they identify a single item on the page. We lost something when we stopped using the language as it was designed.


I think the issue is that CSS is somewhere between unopinionated and encouraging complexity. Is that the best kind of selector? Certainly not, but in Bootstrap, one of the most widely-used CSS packages, it was trivial to find a similar one: `.table-striped-columns > :not(caption) > tr > :nth-child(even) `, so it definitely happens a lot.

Also a lot of it comes down to the original plan to have semantic HTML, which ultimately failed. But we rarely talk about whether semantic CSS is possible, I think the assumption is that it is. But IMO it's not practical because of the poor specificity controls. That's something I'd do different, add in a manual specificity level (like how z-index has manual weights).


I don't see so many problems with that Bootstrap selector. It's long, but right from the first class to the final element selector it's fairly obvious why that clause exists. In fact, the left-most one explains all the rest: from reading it, I can tell what it does and _why_ it's that long. Your earlier example, much less so.

> the original plan to have semantic HTML, which ultimately failed

I feel that browser developers still build browsers with this in mind. You can see this expressed in the default ARIA roles, for example. I feel that developers of large web apps are the ones who aren't thinking about the semantics the right way and who think semantic HTML has failed. It didn't, they gave up trying to use it. Most of us think only in terms of using JS to render anonymous boxes, rather than about outputting an implementation of an underlying information design. Browsers and HTML still supports that very well. We're wrapping all the conceptual design up into JS and components, and treating the HTML + CSS as some kind of dumb output... when in fact that's the browser's job, and it hides that dumb render from us on purpose. If you use your web framework to output an expressive information design, the browser will render it just fine and more of your code will make sense (IMO).

I don't think you gain much from allowing manual specificity. No-one finds managing z-indexes easy. In fact, I think manual specificity would accentuate the problems we already have. You can fix most z-index issues by not using it, because the stacking is mostly handled already for you by the browser. Imagine trying to find the "specificity 203" in your codebase, or to establish a pattern for which disparate Thing A on the page is the same specificity as a Thing B. It'd be harder than the current system, I"m sure.


Any developer describing this code as bad code has probably never seen bad code.


Bad code is innovative for no reason. Complex for no good reason. Hard to reason about (what does foo > * + * mean? and what happens when the writing is Chinese, exactly?).

These various CSS states would be much better expressed as named properties of React components. That way there would be explicit branches instead of allowing the CSS selector engine to do some implicit insanity.


> what does foo > * + * mean?

To be fair, this is the first time I see this but I immediately got it. So might not be as hard / innovative for no reason. I believe someone working often with CSS should be able to parse *+*, and if not, take the time it takes once to understand it. It's nothing that implicit.

Anyway please please don't pull off a React to avoid CSS, just comment your CSS instead, or find a clearer way to write it if you prefer (:not(:first-child)).

This styles documents (puts margins on paragraphs), we don't want an app for this. And even for a React app, we have a nice language to style stuff at our disposal that our browsers are incredibly efficient at evaluating, I don't think reinventing CSS in React is a good thing.

React will die before CSS anyway.


Please do. I'd much rather see a system built in React with explicit logic, math, and names, than an implicit pile of obfuscated rules that are impossible to tweak without causing a cascading failure.


Maybe you wouldn't if you had to pay for all the electricity that's wasted by using React.


Something tells me React uses less electricity than CSS `*` selectors.


How is ".stack > * + *" advanced? This is CSS 101.


I was more referring to margin-block-start, var (with two arguments), gap... Also, no, sibling selector wasn't really supported very well until 2012, so only in the last ~30% of CSS's lifetime.


Shout out to Andy and Heydon for their book Every Layout. It made CSS sensible to me.

By no means am I an expert, but I thoroughly enjoy writing CSS using techniques they teach in the book. Somehow, it helps me to think of CSS as a constraint programming system. Combinators are that --- layout constraint directives.

Architecturally, I really like the choice of pulling apart CSS in terms of structural definitions (Stack, Box, Switcher etc.), style definitions (applied to leaf nodes as far as possible), and global rules, ratios, and units (e.g. modular scale).

I think the system composes beautifully and lets me do a lot with a very small set of core definitions.

My personal site uses those techniques: https://www.evalapply.org/static/css/style.css

edit: formatting


Here are my favorite 4 lines of CSS:

    *, *::before, *::after{
        box-sizing: border-box;
        overflow-wrap: break-word;
    }


Personally, I dislike it.

Firstly, it’s too general. Most boxes don’t care which sizing model is being applied. I have found that I want explicit control more often. To be fair, I learned web development when I had no choice but to work with both models so I don’t find the switching much of a burden.

Second, I’ve seen * { box-sizing: border-box; } cause whole areas of the screen to disappear due to a bug in Chrome. This was years ago, so those bugs have probably been fixed, but it was such a significant problem to the layout and caused by such a generally applied rule, that it’s put me off.


There have been countless times when I was fighting with some margin / size issues, only to notice a * { box-sizing: border-box; } was breaking my expectations.

I learned CSS with the W3C / standard box model, in a time where we had to deal with the buggy IE behavior which was essentially box-sizing: border-box. Now, we have the choice of specifying which box model to use and is seems many people prefer box-sizing: border-box. I'm definitely used to the standard/default one more but odds are someone overridden this.

I don't know which one is better but now we have two existing box models, and depending on the project you work on, the default one depends on what was decided when starting it. It's weird, before it was depending on the browser in use, not the project :-)

I'm not sure how this interacts with libraries / components you import that might not expect such a global setting. I guess you have to be very defensive about this when writing a component. And it seems the expectation is that the component writer should be defensive rather than the website / app builder be careful not to break stuff with global rules. Which is also weird to me.

The spirit of the cascading styles was that you could style and customize everything and that stuff you import would adapt to your styling, but we are rejecting this idea by going out of our ways fool-proofing everything and making nothing customizable. I understand why (it's hard to support everything and ensure no breakage), but this still feels backwards to me.


I'm first and foremost an Android developer and I only started serious web development several years ago. My expectation from the Android view system is that padding is inside the box. The CSS default of it being outside makes no sense to me and I can't think of any use cases where it would be beneficial. The overflow-wrap is also obvious — there are no sane use cases where I would want the text to sometimes overflow the element bounds.


This reads like a haiku.


All these comments complaining about complexity of the selectors - if you haven't read the MDN page on selectors in the last 5 (10?) years, perhaps now is the time?

We were begging for these features for years and it was a nightmare waiting for enough browser support.


While this is clever, it's rather difficult to understand and maintain. Imagine having to sift through a CSS file full of > * + * and assorted symbols and figuring out what they mean (and seeing this article, it's clearly not that simple to explain in documentation either).


It might help to read the original article that was published nearly 10 years ago [1].

[1]: https://alistapart.com/article/axiomatic-css-and-lobotomized...


You might as well complain that x = y + z is bad code because you have to figure out what the symbols mean.


In case you had to look it up (I did), the lobotomised owl: https://alistapart.com/article/axiomatic-css-and-lobotomized...


Mine's display: flex, flex-direction: row, justify-content: space-between


I was hoping this was addressed in the article, and it was, under "Why use margin and not gap?" A number of arguments are presented (including that, at the time, it wasn't an option) and the confluence of those make the owl compelling.

I think I'd probably opt to buy into flexbox/grids and gap for greenfields projects, but I do appreciate the notion of flow axioms and styling the space between adjacent items.


Absolutely. You can usually drop the direction though as it's the initial value. But flex is so powerful for layouts, I'll slap it on almost anything, and barely have a need for grid.


You can now even use `gap` on flexbox.


agreed. grid & flex, combined with a standard set of custom spacing/gap utils for your design system are making life so easy when trying to build coherent layouts.


This immediately made me think of Dave Matthews Band...


I think that this sort of CSS is smart being put in some kind of reset.css, being a base for the design, rather than being used occasionally while developing. In both cases, it would be smart to have explicit comments describing what the snippets do.


Very nice. I like the elegance of owl selectors, particularly when describing the spacing relationships between adjacent siblings.

It helps avoid bleeding issues of space which used to require :last-child / :first-child / :only-child overrides.


This is basically a frontend person looking for some validation that they are so smart.


Respectfully, I'm finding the value of this blog post extremely difficult to understand. The author's arguments against gap just don't make sense and I question the logic of their entire design philosophy - the author dismisses gap and says 'The parent is in complete control and the child elements have no say in what gap is at any given moment."

...but that's exactly how a sound design system SHOULD work. Structural components (containers etc) SHOULD be in complete control of child distribution. I simply cannot imagine what a nightmare having to manage margin at an atomic component level for everything would be instead of just spelling out a few container components for them all to live in.

Furthermore the resistance to using the two display types that utilise the gap property (flex + grid) is absolutely bizarre. No sane person would avoid these tools as web dev without them is messy and utterly maddening, so this resistance makes no sense at all.


Simply: gap wasn't an option when this was initially conceived.

> The author's arguments against gap

I don't think it's a argument against gap. It's that this technique is advantageous in some situations. It is stated later on that grid is his choice for bespoke layouts.

> what a nightmare having to manage margin at an atomic component level for everything would be instead of just spelling out a few container components

This is the essence of the technique. Specifying `* + *` allows controlling the layout of adjacent elements, rather than an atomic element. I think this would all make more sense after reading some of the precedent material: https://alistapart.com/article/axiomatic-css-and-lobotomized.... (It's actually pretty succinct, so rehashing it here wouldn't add anything.)


As someone who decidedly likes CSS I think something like this is "too clever". There are some things that should be as boring as possible. Layouts themselves are already a complex matter, so even the most boring and readable css will not be totally boring.

When I was younger I also had a phase where I liked to do things like these because I could. Nowadays I do things in a way that doesn't waste my own (or other people's) time by being too clever


For me it would be:

border box, flex, the trick to make a square with ::before and content/padding 100%


you will love aspect-ratio: 1/1;


.prose :is(h2 + , h3 + , h4 + *)

A person which values their sanity would not write this in CSS.


oh,I was expecting this.

@tailwind base;

@tailwind components;

@tailwind utilities; :)




Consider applying for YC's W25 batch! Applications are open till Nov 12.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: