The title is not “clickbaity”. It’s how I feel.

The situation

I work on a large site’s component library.

  • The company is split into many teams. Some teams are in other buildings, some are on the other side of the globe. They all use these components.
  • The components are currently implemented in React. Nothing else, though: no Redux, CSS-in-JS, or whatever.
  • ≈500kB of minified + gzipped JavaScript. (Half of which is react-datepicker. Come on desktop Safari, support <input type="date"> already.)
  • And the CSS:
    • Uses SUIT CSS — a way to name component classes to help maintenance.
    • Also has normalize.css and some base styles; :link colors and such.
    • ≈8.5kB when minified & gzipped.

Now, this component library is a little unloved. A single developer started it in his spare time, then it grew as others contributed in an organic, piecemeal way. I was the first employee assigned to work on it ever.

That means its API and code style were wildly inconsistent. And as for the CSS…

The CSS was pretty bad

Allegedly it used SUIT, but there’s many, many comments instructing the selector linter to look the other way. Among those selector sins:

  • Descendant wildcards, like .Input *. (CSS selector performance isn’t a big deal… except for descendants, and especially wildcard descendants.)

  • Abusing the & nesting selector into long combinations like .Tooltipped .Tooltip .Tooltip-header .Tooltip-title — needlessly slow and specific.

  • Complete misunderstanding of inheritance, like .Page-content * { -webkit-font-smoothing: antialiased }.

  • Complete misunderstanding of SVG, like .Input-validationState path { fill: red } when the SVG icon had fill="currentColor".

The one good thing was it contained not a single !important.

The opportunity

Then, to everyone’s great surprise, the component library had a team assigned to it. The consensus was that some sort of rewrite was nigh.

Personally, I was all for investigating ways to prevent those CSS tragedies from reoccurring.

I had some requirements

  • Our site is already prooty slow, so I really didn’t want to make it worse.
    • Shouldn’t require lots of descendant selectors, like Pure.
    • Must support server-side rendering.
    • Must support Critical CSS.
  • If the approach is unusual, it’s harder to teach newbies how styling “should” be done.
  • I wasn’t concerned with style isolation. In the six months I had worked on the library, there was a CSS conflict with other code exactly once. The amount of times some interfacing JavaScript had broken? Way higher.

Enter Tailwind

The new team has investigated vanilla Shadow DOM/Web Components, SkateJS, Stencil, styled-components — anything vaguely interesting.

One experiment was Tailwind, which seemed like a mature and tweakable Functional CSS approach.

I didn’t hate it at first

I’ve been leery of Functional CSS ever since it got popular enough to upset old WaSP members, but as we investigated Tailwind, I grew curious.

After all, it does suck thinking up a new class for a subcomponent when I just need to center some text. Reusing .text-center instead of adding .FooWidget-bar { text-align: center } is certainly appealing — maybe the trend continues to its logical conclusion?

By the way: it’s not about “semantic classnames”

Meaningful and descriptive selectors are great for HTML’s reason for being: content. Content outlives everything, including CSS, so classes that are intelligible 10 years later and unobtrusive selectors are important.

For example, say you want the first letter of every article red and bold. You do it with functional CSS classes. Eventually the design changes, so now you have to Ctrl+F through all the content. What you probably wanted was something like article::first-letter, not <span class="color-red weight-bold">.

(People who say I should have code to abstract the styles away instead of stashing the classes in the source content are encouraged to tell me how that isn’t reinventing CSS.)

However.

UI components are so inextricably tied to how they look that changing their markup is no harder than changing their CSS. So this argument didn’t matter for me when considering the component library, since all its HTML is dynamically generated.

My optimism was unfounded

Anyway, the team rewrote some of the components with Tailwind.

I didn’t like the results.

Worse performance

This more than anything else was what soured me.

Larger JavaScript payload

Styling complexity has gotta live somewhere. But I don’t think it’s a win to move it from the CSS that:

  • Can be downloaded/parsed in parallel
  • Can be inlined/pushed/preloaded
  • Gets faster the more threads you throw at it, like with Stylo and Servo
  • Styles incomplete HTML as it streams in from the network

…into the language that:

  • Is a notorious performance hotspot just to parse (hence why WebAssembly exists)
  • Goes through multiple compilations, eating battery all the while
  • Is already implementing the markup and behavior, dooming the download to be serial

More memory usage

Folks usually cite increased HTML filesize with Functional CSS, but I’m unsure that has a real performance impact. Show me the numbers.

In the JavaScript, however…

Firefox and Chrome recently finished a race between their style engines to use less memory. When the dust settled, each selector required a whopping two bytes.

I don’t think it’s possible to be that efficient in JavaScript.

Anyway. In the existing library, we had logic that looked kinda like this:

  const classes = `Button Button--${props.kind}` + (
  (React.children.count(props.children) === 1 && React.children.first instanceOf Icon) ? 'Button--icon' : ''
);

(The faffing about with React.children is one reason I prefer Preact, where children is always an array.)

The equivalent className juggling with Tailwind:

  const buttonKindStyles = {
  primary: 'bg-blue border-white hover:bg-darkblue focus:bg-darkblue …',
  secondary: 'bg-white border-blue hover:bg-gray focus:bg-gray …',
  ...etc
}
const isOnlyIcon = (React.children.count(props.children) === 1 && React.children.first instanceOf Icon);

const classes = `rounded border focus:outline-ring …` + 
  buttonKindStyles[props.kind] + 
  (isOnlyIcon && 'px-4 py-6 rounded-md');

Functional CSS, in JSX, manufactures a bunch of strings every time render() happens. (That is, constantly.) Those strings take up memory in the incoming component tree, the diffed-against component tree, and in the DOM. I don't even know how many copies are littered amongst the heap and stack. (Sure, hopefully the browser will intern them all… but prove it.)

I only needed one test in various browsers’ Memory devtools to see that Functional CSS consumed more memory. That also made the garbage collector even crankier than usual, freezing frequently.

Massive output CSS

By default, tailwind.css is 36.4kB minified and gzipped. That’s a problem.

For critical CSS, we need visible content within the first 14 kilobytes — that’s how much content fits into the first salvo of TCP packets. That includes the inlined CSS, the HTTP headers, everything else in the <head>, and then, finally, some content to style.

The Tailwind team are aware of this, and address the problem in the documentation. You can use @variants to limit the class output and prune it with PurgeCSS.

Problem is, as the component team, we can’t count on our consumers using PurgeCSS. PurgeCSS is also surprisingly hard to use “properly” (string manipulation foils it), and other tools like UnCSS take a long time and are no less error-prone.

Regardless of the technical details, my main objection is philosophical. Tailwind created a lot of work for us and our peers that didn’t exist beforehand. Remember, we had 8.5kB of CSS — and that was legacy, bad CSS.

8.5kB of CSS is nothin’. We can even automatically omit a component’s styles if it wasn’t imported. The problem was already solved by SUIT.

Too inflexible to meet developer needs

  • Weird pseudo-elements, pseudo-classes, vendor-specific CSS, hacks, font fallbacks, and so on
  • One-off media queries
  • One-off positioning. I know, that’s kind of the point. But it’s like the Play button principle — the human eye isn’t logical.
  • Arbitrary z-indexes for dealing with third-party content
  • CSS grid positioning, specialized flex values,
  • High-Contrast Mode and other accessibility thingos

Now, this lack of flexibility was one reason Functional CSS was appealing: no more z-index: 999999 or one-off paddings. But there’s other fixes to those problems: linting, audits, a design system…

To be judgmental, functional CSS seems like a universal solution only if you don’t know much about CSS.

But that’s okay. Nobody said you had to only use Functional CSS for everything, even if it’s heavily implied you should. You can have an additional simple-elegant-and-wrong.css file.

Not widely compatible

Browser devtools optimize for standard CSS — toggling properties and media queries, color pickers inside individual rules, etc.

Image of browser Styles pane of devtools

Instead of those nice toggle checkboxes, we had to double-click on the class attribute and manually type changes, otherwise we would, say, turn off bold text everywhere.

And that’s not the half of it. Other things we missed out on:

  • Postprocessing like minifiers and Autoprefixer

  • Autosuggestions in any text editor

  • Style linters, both in-browser and out-

  • Bottleneck of one source of documentation, instead of everything ever written about CSS

  • Another layer of abstraction: you have to learn the abbreviations that correspond to the style names/properties you already know

  • Spidering with wget

  • Text editor’s built-in checkers; VS. Code lets you know when you have position:absolute and vertical-align in the same declaration, because the latter is irrelevant when absolutely positioned.

  • A zillion other things I don’t know, because CSS is an old, widespread, well-understood declarative language that tools have dealt with for decades.

Does Functional CSS even help with the hard parts of CSS?

One of our developers who admits he’s bad at CSS really liked Tailwind. During the experiment, he wrote code like this:

  <div class="flex w-full flex-row justify-start">

That translates to the following CSS:

  .whatever {
  display: flex;
  width: 100%;
  flex-direction: row;
  justify-content: flex-start;
}

If you don’t see the problem: all 3 properties after display: flex are default and unnecessary.

Sure, specificity sucks. The cascade can be unclear. Accidentally leaking styles happens without discipline.

But browser devtools do an excellent job showing which styles override others. It shouldn’t be hard to write automated tests for this, except that most test frameworks ignore the cascade.

To me, the hard parts of CSS are more like:

  • Vertically aligning an icon with some text across OSes, font metrics, and fallback fonts (or anything to do with the nitty-gritty of display: inline)
  • Block formatting contexts
  • Robust, complex reponsiveness
  • Getting the layout you need without (or with) wrapper elements
  • Jankless transitions and animations on wimpy devices (Chrome’s dirty region unioning is the worst and no other browser seems to have the problem as bad)
  • Browser support and progressively enhancing around it

That is, the hard parts of CSS are how the properties interact, not the selectors. Functional CSS did not help with these problems.

There is a place for single-purpose classes — utilities

So despite everything I just griped, I’m not wholly against Functional CSS. Sometimes you do want a laser-focused, single-purpose class to sprinkle throughout your HTML.

SUIT has them already.

I feel there’s an easy way to tell when functional classes are a good fit: does that style never change? Functional CSS feels wrong in the following situations:

  • Interaction states: :focus, :target, :user-error, etc.
  • Media queries
  • Transitions and animations

In conclusion

Draw your own conclusions. I’m just some guy.