Hacker News new | past | comments | ask | show | jobs | submit login
The Evolution of Scalable CSS (frontendmastery.com)
108 points by remrem on Nov 12, 2022 | hide | past | favorite | 56 comments



Reading an article about the evolution of CSS and ending up at…Tailwind…really feels like a bait and switch. I don't consider Tailwind at all a "solution" for writing scaleable CSS. It's a non-standard DSL for providing styling commands to a build tool, and the HTML/CSS output overloads class attributes everywhere in an absurd way. The "just use @apply" retort is also a non-starter if you want to write stylesheets that aren't vendor-locked to Tailwind's build tooling.

I'm actually working on a large design system that's in the process of stripping Tailwind _out_, because it's in fact not scaleable if you want to provide vanilla component styling APIs via custom properties. And as some others here have mentioned, we're not even talking yet about web components, shadow DOM, shadow parts, etc.

Scaleable CSS should adhere to the actual spec of CSS and how its evolving in browsers. Tailwind is a massive, non-standard deviation from the development of CSS.


Scaleable CSS should adhere to the actual spec of CSS and how its evolving in browsers.

The more experienced I get, the more strongly I agree with this sentiment.

Modern CSS provides numerous features to solve real-world problems, from building practically useful layouts and effects to organising and efficiently representing the style rules for large and complicated sites and applications. These features enjoy broad support in browsers, so unless you’re among the unlucky few who really do still need to support legacy browsers like IE or early versions of iOS Safari, you can probably use them as-is without any special tools or heavyweight build processes.

One genuine, qualitative improvement I’ve seen in modern tools is CSS modules. Those let you tidily avoid the old namespacing problems for styles used locally, while still having global styling available for your overall design system, colour scheme, typography, etc. That’s often useful, and as far as I know it can’t yet be done with widely-supported native CSS alone.

If you stick to native CSS and maybe some light tooling like CSS modules, SASS and/or PostCSS, you can work with negligible lock-in, excellent portability and longevity in all the code you write, and access to the full power and flexibility that CSS offers both now and in the future. That means everything you do is effectively scalable by default.


Tailwind's just a shorthand style for writing [a particular snapshotted older version of] CSS based on the theory that saving on keystrokes is worth more than understandable naming. It then leverages this brevity to let people over-define styles directly in the HTML, which would otherwise take paragraphs of normal CSS and therefore be unwieldy to do (so you'd just alias it into a new CSS property in a separate file). That's about it.

While brevity is a nice feature, and we should always strive to make the dev experience as elegant and simple as possible, methinks Tailwind is just asking to become yet another dead-end standard which I very much hope has a standardized compiler back to vanilla CSS. (Does it not?)


Tailwind takes a radical approach to the cascade by allowing devs to completely ignore it at the cost of losing the power of using it.

It eliminates the need to name things at the cost of dealing with unreadable and unmaintainable class soup.

It provides a useful set of constants trivial to create in any non-tailwind css codebase.


> Tailwind's just a shorthand style for writing [a particular snapshotted older version of] CSS based on the theory that saving on keystrokes is worth more than understandable naming.

Tailwind takes the "cascading" and "sheets" out of CSS. Instead of describing what types of things look like, it forces you to describe how each individual instance of everything looks. Which is a valid thing to do, but it's absolutely not a parallel track to the purpose and paradigm of vanilla CSS.


Yeah but you can do that with CSS, you'd just never want to as it's too verbose and a pain to change en-masse compared to abstracted classes. Those concerns don't go away with Tailwind - they're just less trouble to type.


Tailwind is neither a DSL nor is it any kind of standard. It's literally just utility classes. The "compiler" is really just tree-shaking.

I feel like I'm taking crazy pills.


Tailwind stopped being "just utility classes" a long time ago and it's really become its own language with TW-specific terminology.

https://tailwindcss.com/docs/adding-custom-styles https://tailwindcss.com/docs/hover-focus-and-other-states


I hope you're right, and it's always easy to automatically replace Tailwind with vanilla CSS, but I'm not so sure anymore


The conclusion I took from the article was that CSS simply isn’t scalable. I’ve been hearing about web components for years and nothing seems to have materialized. I’ve never built something where I thought to even use shadow DOM. Tailwind is the only solution that meets the stated aims. If tailwind doesn’t adhere to the dogma for how CSS should work in principle, well that’s a lovely problem to have.


"Nothing seems to have materialized..." --> https://nordhealth.design/ -- these are all custom components. And here is D. Darnes explaining in part how they get things done: https://web.dev/custom-properties-web-components/

CSS is scalable. You have to actually know CSS in order to accomplish it. I've been here since mid-90s, when CSS first existed. It isn't until the past four or five years that things have really blossomed into (imho) what it should have been from the outset.

Unfortunately, since we have had to use filler tools for things like layout (tables, hacked flexbox for grid), CSS itself has taken a beating in perceived usefulness/weildability. I believe because of THAT, fewer and fewer people have decided to actually learn CSS.

CSS is a robust language. It is programmable. It is scalable. You just have to learn it, practice it, and approach problem solving from the top down, bottom up, and sometimes sideways. :-)


Well, CSS have enough problems to probably suggest it failed.

It can work, but it failed for massive amount of people, it doesn't make it a successful paradigm.


> Let’s take a break from all these principles and architectures, and remember that CSS is ultimately about implementing visual designs.

I enjoyed the overview (though the web component/shadow Dom bits others noted here would have been nice) but I feel like there's a missing realization lurking at the center of this history: the separation of concerns in the web stack was designed for documents, not layouts or visual designs.

Much of the struggling we've seen over the decades is because the separations aren't aligned with how we're using them. It makes sense for documents to have separate stylesheets. It doesn't make sense to fragment the implementation of a layout across several files and complexly-interacting languages.

This isn't really a fault of the article. I only just realized this in the past year myself and I've been writing html since the all caps era. I took a first swing at writing about this recently in: https://t-ravis.com/post/doc/what_color_is_your_markup/


> I feel like there's a missing realization lurking at the center of this history: the separation of concerns in the web stack was designed for documents, not layouts or visual designs.

I think that realization had solidly set-in throughout the industry by 2010 or so, and why so much effort over the most recent decade went into things that do lend themselves to an app-like focus such as native flexbox & grid capabilities, shadow dom, and even recent things like :has().

But personally... I think that "apps as documents" paradigm has actually worked out pretty brilliantly, even considering how squirrelly CSS can be. The inspectability of the UI layer is one of the reasons why the browser is so much more than the VM that lived, and HTML/CSS has so much flexibility that many native UI toolkits lack. It's not right for every situation, but for the common case I think there are reasons it's often what people reach for first.


It's an incredibly flexible toolkit. Not calling it bad!

It just isn't cut along the seams we'd cut it along if we were building it fresh.

There's tons of evidence in the record-of-innovation that people are iterating around these problems--so I suspect there are plenty of people who have indeed internalized this to some degree. But I also see a lot of continual coverage that indicates this realization hasn't diffused.

(I don't want to pretend to have invented the wheel, but for example I see the fights around semantic VS functional tailwind-style css as hinging on this. Functional css makes sense for layouts as it collapses pointless separation; semantic classes make sense for documents which are more likely to evolve or get restyled without new markup.)


Jesse James Garrett (the early web luminary who coined the term "ajax") was already talking about the dual purposes of the web in the year 2000. He called them "Web as software interface" and "Web as hypertext system".

In The Elements of User Experience (2000) he writes: "The Web was originally conceived as a hypertextual information space; but the development of increasingly sophisticated front- and back-end technologies has fostered its use as a remote software interface." [0]

[0] http://www.jjg.net/elements/pdf/elements.pdf


> The Evolution of Scalable CSS

The evolution of practices described in this article is followed up to about ten yeas ago... and then stops, followed by Tailwind. Where is consideration of web components with shadow DOM, which solve the CSS encapsulation problem, the naming problem, and the dead code problem? Where is treatment of CSS variables that pierce the shadow DOM and allow global theming of such components? They are the next step in the evolution of scalable CSS, and yet they are noticeably absent from the discussion.


We wrote about our journey[0] to sanity after Tailwind:

The answer is to do it all in strategic moderation: Use a subset of tailwind just for spacing and layout and revel in simple things being simple. Use modular CSS for UI patterns and revel in the readability of your HTML and visual consistency of your UI. And then use custom scoped CSS where required, and revel in interesting things being only as hard as need be, without ruining everything else along the way.

[0]: https://frameable.com/company/tech/how-we-found-sanity-in-a-...


Great post. It's similar to CUBE, https://cube.fyi and the creator of CUBE even wrote about using "Tailwind as the U in CUBE"


> Where is treatment of CSS variables that pierce the shadow DOM and allow global theming of such components?

I feel like, at least with that one, was left behind because there's some work left to do in that regard.

For the redesign of my portfolio, miler.codeberg.page, I used icons I did for the previous one (and did some new ones, but all are a bunch of <symbol>s in a single file) but instead of "injecting" the SVG code in the HTML I tried to use them from CSS as pseudoelements (as a 'content' fragment identifier) and style them (stroke color).

The state of things right now, as I learned, is that it's impossible to do that - neither with some obscure parameter within the SVG fragment identifiers. I had to do some crop filter trickery to "change" their stroke color.


> I tried to use them from CSS as pseudoelements (as a 'content' fragment identifier) and style them (stroke color).

Isn't this just the general problem of loading an svg as an image via a link as opposed to injecting it into the DOM as inline svg? How would any of the existing CSS approaches help (or how would web components hider) this one?


Indeed. It's web components and the shadow DOM that fix all of this.


There’s also no mention of CSS Cascade Layers [1].

[1]: https://css-tricks.com/css-cascade-layers/


The article does mention them... I'm just finding out about this, it could be very useful!

https://frontendmastery.com/posts/the-evolution-of-scalable-....


> Tailwind and CSS in JS libraries that pre-compile to Atomic CSS solve the problems of bloated CSS files full of duplicated rules.

> With Atomic CSS, the growth of CSS is tied to the number of unique styles used, not the amount of features developers are shipping.

> For example, it’s common to reuse certain properties like flex everywhere. Rather than have these duplicated in stylesheets under different class names, we only pay that cost once. This is true for each property/value combination.

This is true in theory and when you stick to the very basic stateless utility classes. Check the CSS of any larger site using Tailwind and search for a CSS rule, you will see things like

    .opacity-100 {
        opacity:1
    }
    .hover\:opacity-100:hover {
        opacity:1
    }
    .group\:focus-within .group-focus-within\:opacity-100 {
        opacity:1
    }
 
and that's not counting media queries, dark mode, etc. In practice, the global CSS file that you load for every single page does grow for every feature you ship, since you use more of Tailwind.


My favorite implementation of CSS systems has to be elm-css. Your CSS is just normal elm code included in your view code. E.g. everything is an expression, so can import reusable CSS, compose it from different blocks, have functions calculating the CSS runtime etc. Then runtime the rules are compiled to classes and applied to the elements.

One nice thing about this is that instead of using classes, you can instead statically import the definition and apply it to your element, making it even fail compilation time if you remove some global styling some component still relies on.

And the CSS itself is typed. So the project will not compile if you write invalid css.


For anyone else trying to understand how this fits in, it's either the "Inline Styles" section or the "CSS in JS" section, as long as you use typescript and follow the same rules mentioned here


If I were to explain it in terms of react, it would be to use <Component style={{mystyle: someValue}} /> everywhere. Except that you also can define nested stuff, media queries, hover/active states, pseudo-elements etc. So yeah, syntactically a bit more like CSS in JS / JSS, but much more ergonomic, and much more easily composable.


It sounds like you have experience with the string-heavy CSS in JS. Here's some examples showing how this can be achieved using `csstype.Properties` objects: https://emotion.sh/docs/typescript

I'm sure you can imagine how easy it is to compose functions that accept and return `csstype.Properties` objects, and then at the end, you call `styled` with it to make a styled component. You can even pass styled a function, which takes in runtime-adjustable props, and of course, that function can itself use other functions, passing around those same or some derived runtime-adjustable props.

That said, I have no idea what you mean when you talk about ergonomics being better, can you elaborate? I re-read your initial comment but there wasn't anything in it not achievable in TS using the kinds of things mentioned in the "CSS in JS" section, perhaps there's something else I'm just not aware of? I do not know elm, let alone have experience writing it in non-trivial production codebases, so a concrete example would be great


Thanks for some explanation. And yeah, I'm probably not up to date on CSS-in-JS, just remember it being really clunky to use before I ended up in an elm-shop a few years back. I'd say that emotion does look similar to what I've described, and nearly there. And I don't think there is anything doable in elm that's not doable in TS/React (it's rather the opposite, elm is the restricted one, mainly on purpose). My point about ergonomics was mostly that it's just elm all the way. No strings or objects or new concepts. The CSS you write in elm is mostly indistinguishable from the other elm view code (html etc) you write, it's all the same syntax etc. While for emotion and others, there is a clear divide in what's React and what's styling, and difference in how you do stuff.


Fwiw, most CSS-in-JS libraries don't use `style` directly, and so also allow the same features. One thing I've seen often is to use a `css` pseudo-prop so that it looks a bit like using `style` (but you've still got access to the full force of normal CSS rules).

My favourite variation of this so far has been typestyle, which I believe works along essentially the same principles as in Elm.


Good article. I would have liked to see the article sternly advise against all CSS naming schemes like OOCSS. All CSS classes go into the global namespace, the solution is obviously not hand written naming conventions, it’s getting rid of the global namespace itself. OOCSS is like saying “we have to put our entire program in index.js at the top level, how do we name things to avoid conflicts?”

CSS is a fully solved problem: CSS modules combined with global utility classes. You probably want sass too, but the important thing is you’re writing CSS, and font have to figure out the DSL, translation, and plugins needed to use CSS features.


It's solved natively without build tools too: native CSS module scripts for importing and shadow DOM for applying the styles with scoping.


There's still no good way of publishing an npm package with multiple components, stylesheets per component, and transitive consumers using only the stylesheets for the components they reference while rendering fully server side. CSS is still not scalable.


This is what native CSS module scripts solve.

You can import CSS directly into JavaScript modules:

    import styles from './styles.css' assert {type: 'css'};

https://web.dev/css-module-scripts/

Live example: https://lit.dev/playground/#gist=f0bdd3b5db5e4a404297e695305...


SSR was mentioned though.


There is actually a W3C working group working on a standardised design token schema specification and on adoption by design tooling.

Several big names such as Adobe are actively involved, so this has some weight to it.

https://github.com/design-tokens


I saw the link to CSS Garden in the second section and was happy to see the site is still up and working. Separation of concerns is such a powerful idea — losing it by tying styles too tightly to markup is a huge step backwards.


I really dislike Tailwind. As someone who knows how to work with CSS, it feels like a simple vendor-locked version of CSS you throw onto your codebase and pollute your DOM with divs that have 40 classes. From my experience, working on smaller projects and bigger CRMs, single file components with separate template, logic and style areas are the best when it comes to scalable components.


FYI, component styles in Sciter can be placed directly in JS module using `CSS.set` string literal (gets compiled at runtime to special style set object).

The style set is a collection of styles rules local (rooted at) to the component. :root denotes the component to which styleset is applied.

The style set definition is strictly local to the component - rules inside it do not pollute global style table. That is close to <style scoped> to anyone who remember such thing.

Here is how styled component.js module looks like:

    const padding = "12px";

    const styleset = CSS.set`
      :root {
         color: var(--theme-color);
         padding:${padding};
         background: var(--theme-back);
      }

      :root > em {
        color: gold;
      }`

    export class TestComponent extends Element {
      render() {
        return <div styleset={styleset}>
          Hello <em>Embedded</em> Style Set
        </div>;
      }
    }

IMHO, that's the best of two worlds - components may have local styles principal for their operation, and to use global styles that define macro properties like themed colors, etc.


This is almost exactly how Lit works in browsers. CSS is included with a css`` template string, that contains standard CSS. :host is used to select the host element, as is standard.

    @customElement('simple-greeting')
    class SimpleGreeting extends LitElement {
      static styles = css`
        :host {
          display: flex;
        }
        p {
          color: blue;
        }
      `;

      render() {
        return html`<p>Hello, World!</p>`;
      }
    }
So you get CSS that loads with components, is scoped, and bundles without plugins. It's pretty great.


Yes, that's close.

Only one thing: that static styles construct shall eventually go into a separate <style> DOM element inside shadow root. Probably not a big deal if there are not that many such components.


Actually, we add the stylesheet objects directly to the shadow root, so they're shared and there's no <style> tags (on supported browsers).


Tailwind is the solution to most CSS issues. You can also add so many customizations into their config file. There are just a few things that can’t be done with tailwind natively.

With tailwind and react you now only write JSX files. Not separate files/sections for HTML, JavaScript and CSS your mind constantly needs to connect, although they are split up. Huge plus for someone with a goldfish brain like me, that’s getting super confused with more then 5 files open in the editor.

And with tailwind 3.2 they finally allow multiple config files, so you can easily have multiple tailwind instances inside one project. For example one config for the website and another one for the admin-panel.


Is there a resource designed for both designers and developers to aid adopting design tokens?


This W3C Design Tokens working group might be of interest to you: https://github.com/design-tokens

Ever so slightly more details in my own top-level comment https://news.ycombinator.com/item?id=33575191


Isn't this is a solved problem? Look at tools that provide themes. You can't use anything like semantic css or business related naming. It has to be widget based styling and that seems to be the end of it.


this is a great history of the development of CSS up til Tailwind. I think there us one more subtle benefit to tailwind not mentioned:

Using "design system" values - every color and width variable comes with well chosen constraints!

(from my take on this https://www.swyx.io/why-tailwind)


I never understood this with BEM:

  .nav {
    &__link {
    }
  }

Why not this?:

  .nav {
    a {
    }
  }


To avoid CSS inheritance & specificity, the "Cascading" part of CSS is the actual root of all evil. If you think about it, all the modern approaches to CSS (BEM, Atomic CSS, OOCSS) try to avoid it all cost.


To avoid CSS inheritance & specificity, the "Cascading" part of CSS is the actual root of all evil.

The Cascade is CSS’a superpower.

If only someone would come up with a way to write CSS to make the cascade work for you, not against you.

Oh wait; that would be Inverted Triangle CSS.

[1]: https://www.xfive.co/blog/itcss-scalable-maintainable-css-ar...


I'd like to believe that, but the industry has proven across the board that inheritance sucks. Cascading to me seems essentially inheritance, so I'm disillusioned, it's not a superpower, it's a liability


Exactly. I've never written strict BEM, mainly just take a few cues from it now and then. When writing components and especially now that we have awesome low-specificity selectors like :where or cascade layers (and soon :has !!), it's really better to just target the DOM structure with semantic/custom element names, parent-child relationships, states like aria, etc. and not saddle every single element with some kind of naming convention. (I honestly don't use `class` much at all anymore.)


How do I disable dark mode on that website? I can’t read text very well in dark mode.


I don't think there's that option, but the page does work well with reader mode.


Thanks. I always forget that reader mode exists in browsers.




Join us for AI Startup School this June 16-17 in San Francisco!

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

Search: