I've been teaching semantic HTML / accessible markup for a long time, and have worked extensively on sites and apps designed for screen readers.
The biggest problem with Tailwind is that it inverts the order that you should be thinking about HTML and CSS.
HTML is marking up the meaning of the document. You should start there. Then style with CSS. If you need extra elements for styling at that point, you might use a div or span (but you should ask yourself if there's something better first).
Tailwind instead pushes the dev into a CSS-first approach. You think about the Tailwind classes you want, and then throw yet-another-div into the DOM just to have an element to hang your classes on.
Tailwind makes you worse as a web developer from a skill standpoint, since part of your skill should be to produce future-proof readable HTML and CSS that it usable by all users and generally matches the HTML and CSS specs. But devs haven't cared about that for years, so it makes sense that Tailwind got so popular. It solved the "I'm building React components" approach to HTML and CSS authoring and codified div soup as a desirable outcome.
Tailwind clearly never cared about any of this. The opening example on Tailwind's website is nothing but divs and spans. It's proven to be a terrible education for new developers, and has contributed to the div soup that LLMs will output unless nudged and begged to do otherwise.
I look at the royal mess that is HTML/CSS/JS as a necessary evil, required when we want to target browsers. To me it's "just the presentation layer".
In my work I put a lot more emphasis on correctness in the db schema, or business logic in the backend.
When it comes to the messy presentation layer I prefer to write a little as possible, while still ending up with somewhat maintainable code. And for this Tailwind fits the bill really well: LLMs write it very well, new devs understand it quick, and it's quite easy to read-back/adjust the code later.
I 100% agree a Tailwind project is not the best way for a new dev to learn HTML/CSS. But then I prefer the new dev to focus on great db schemas, intuitive APIs, test-able biz logic, etc. Fiddling with the mess that's HTML/CSS is not the place where I consider human attention is best spent on (or where developers pick up skills to become much better developers).
I think that upside became more prevalent in the reusable components era, whereas previously CSS was targeting an entire HTML file (and thus the reasoning was more like SQL query than "this one element's styling").
With LLMs I think this upside is much smaller now though.
She writes from a place of vulnerability and honesty. Most people write to sound smart and she writes to say "I don't know it all but there are some things I discovered I want to share." I almost feel like she writes to share things with people she loves, even though she doesn't know them directly.
She spoke alongside Randall Munroe at the last Strange Loop (RIP). Some people waited to talk to him afterwards, but I waited to talk to her. I don't think she got my joke that she should rewrite her bash scripts into perl and for that I'm truly sorry.
Thank you for articulating this!
I'm not Julia, but I'd just like to put down here that this is pretty much my philosophy for public speaking/giving presentations, and I have been trying to instill it in some coworkers who struggle with presentations. It's a great privilege to be able to convey to one's peers and loved ones things that you're (likely) a bit more familiar with than they are and which may help them with some matter.
CSS is a skill just like any other technical skill. If all you do is learn the bare minimum so you can bodge things until you get something that looks right, then your ambitions are going to outpace your ability to keep things organised very quickly.
That seems like a false dichotomy. I'm a huge fan of locality (both in software engineering and in physis) but you can also "localize" your styles by scoping them appropriately. (Modern frontend frameworks typically do that automatically for you at the component level.) There is no need to use Tailwind for that.
It's really frustrating to be talking with someone about Tailwind and CSS, and realize that not only do they not know what "cascading" means, they never even considered the concept might be useful in the context of a stylesheet.
And none of this really violates DRY, your unit of reuse has shifted from a CSS class to a framework component. There's nothing precluding you from using an approach like DaisyUI if stock Tailwind has too much repetition for your taste.
This is what CSS classes were made for. Of all of the arguments in favor of Tailwind, this is the one that drives me battiest. Say what you will about CSS, but "give a name to a re-usable set of styles for a component" is pretty much as fundamental as you can get.
> And none of this really violates DRY, your unit of reuse has shifted from a CSS class to a framework component.
Sure, sure. except for the inline styles everywhere. And the fact that everything is literally being repeated all over the place. But other than that, no repetition!
> There's nothing precluding you from using an approach like DaisyUI if stock Tailwind has too much repetition for your taste.
...and now you have three problems.
Whenever i have written CSS/TailwindCSS which was unproblematic to extend it was when i literally switch thinking to use least amount of properties and let the page flow.
Whenever i see tons of css i know it’s brittle and will cause hours of wasted time down the line to fix something which already should have been fixed.
And when this is pointed out you’ll usually get replies that just hand wave it away as not a problem, as if things like BEM were invented for no reason.
But sure, like most tools, it starts with understanding how it works.
No, I don’t think that’s the case at all.
I think that was true at the beginning. But Tailwind is quickly approaching the multi-headed hydra it was trying to replace.
It gets really easy to lean on class-based CSS and use a `<div>` for everything instead of ever learning what a semantic element is.
And that contributes to other bad habits, like writing a bunch of JavaScript to define behavior that could just be natively handled by your browser.
A weird personal irony is that because no employer has ever asked me to directly write CSS, what's actually made me better at CSS is JavaScript -- namely that my understanding of selector logic has improved a lot after picking up Web scraping.
Recently I've been using linaria which is a drop-in replacement for styled-components (exact same API) but its zero runtime. All the CSS is compiled during build (similar to vanilla extract, panda CSS, etc).
I really prefer things like styled-components or Linaria or CSS Modules where you can just write straight up CSS. If you ever decide to switch your tool you should always be able to just copy-paste your CSS away. You don't get that with Tailwind or "styles-as-objects" stuff (StyleX)
Maybe it's useful for people here. I don't use Tailwind or similar for styling, just CSS with modern frameworks like Astro or Svelte.
For every project I have the following CSS files:
- reset.css
- var.css
- global.css
- util.css
Other styling is scoped to that specific component or layout.
The other thing Tailwind stops from happening: class name bloat. In its absence, agents invent classes such as "card-header-inner", "feature-block-content", "sidebar-item-wrapper" – all separate naming choices. After a few months of development, you accumulate hundreds of classes that are not owned by anyone. The limit placed by Tailwind is in its vocabulary; there are no names to invent. This trade-off described by Julia exists. It's just articulated a bit differently.
<div class="counter-component">
<button @click="count++">+</button>
<span class="count" :data-is-even="count % 2 === 0">{{ count }}</span>
</div>
<style scoped>
@reference "tailwindcss"
.counter-component {
@apply flex items-center gap-2;
button {
@apply bg-gray-800 text-white;
}
.count {
@apply italic text-teal-500;
&[data-is-even="true"] {
@apply text-rose-500;
}
}
}
</style>I'm a fan of removing any dependencies on external libraries and writing my own solution from scratch, but there's a good reason why I decided not to do so with Tailwind: They offer an optimization for production that ensures that you never ship more than the bare minimum of CSS needed. This means you can keep your palette of color, spacing, and other options fully enumerated in `globals.css` and elsewhere, without worrying whether you're using all those variants in production. Moreover, if you're working within a framework, such as Next.js, this minimization step automatically happens when you build, without even having to worry about whether it's happening. This alone is a compelling reason, at least for me, not to migrate from Tailwind.
Also, I've never found any restrictions in Tailwind in using inline CSS that weren't readily navigable, or in implementing really nice responsive grids that handle different screen widths for instance using Tailwind's grid tooling. I definitely have solved each of the scenarios described in this article using Tailwind or a Tailwind-CSS combination, but it's true that they don't have grid-column-areas natively. Still, I haven't yet found that to be a significant restriction in getting responsive grid layouts.
I think the biggest issue with Tailwind is simply that it takes a long time to get used to reading it. We all learn that inline CSS is bad, globally scoped CSS is best, etc., and we get used to seeing clean simple HTML. Then we look at real-world code featuring Tailwind and it just looks so hard to read at first, especially because the lines are so long. I guess I just have been using it long enough that I've gotten completely used to the way it looks, but I do remember it took me a very long time to get comfortable with reading Tailwind. After a long while, I concluded that, for me, Tailwind really is more efficient and maintainable and even more readable, but it definitely took quite a bit.
Why not use native css variables?
> Moreover, if you're working within a framework, such as Next.js, this minimization step automatically happens when you build, without even having to worry about whether it's happening
Again, if you are using plain css I don't think this is an issue. With any modern build system it will spit out css file for that build, right?
> After a long while, I concluded that, for me, Tailwind really is more efficient and maintainable and even more readable, but it definitely took quite a bit.
I think this sentence says it all: Any framework will be "more efficient and maintainable" once learned, even if "took quite a bit".
For tailwind I think it's an abstraction too far, but that's a decision we all do ourselves.
That said, it is a good post putting all those things into one post and give a sane baseline for people who are new to this, or don't know the web's basics well.
Lately I've been enjoying Open Props[0]. It's a library of CSS props/ variables that helps structure a design system. I like it because it's CSS-first, so like OP experienced moving off TW, I've learned more CSS, and it works with the browser not against it. It also provides some sane defaults for anyone less interested in fiddling with precise cosmetics.
- AI already have data about its classes in their training data - No conflicting styles
This means that AI doesn't need to reference any existing stylesheets when generating new styles, which is great for context management.
With custom CSS, you'll have AI read existing stylesheets because otherwise its going to write conflicting styles or rewrite stuff you already have. This can be a problem if you have large stylesheets that take too much space in AI memory.
css applies attributes to objects via graph queries
the queries are tightly coupled to the tree. you must work hard to avoid scatter gun edits now. it doesn't make much sense to have attributes stores in a separate location to their use
it would be like assigning all of your instances attributes using decorators
The same goes for CSS. Everyone bolded, and highlighted their experience with Bootstrap but missed the CSS. I did used Bootstrap, Foundation, Skeleton, Bourbon, and many others, especially when working with the team, so we all can speak the same language. This is true for Tailwind too. I remember when Tailwind was still in alpha and I realized that was the perfect tool to bring the team together and move fast. I was able to use it both as a utility and like most other people as the HTML polluter (but it worked).
If one is keen, it is always a good idea to learn the core - HTML, CSS, JavaScript; all the frameworks that wraps them should just be syntactic sugar. Bootstrap came and went, so will Tailwind.
PS. With AI/LLM Coding Assist, writing in plain CSS is becoming beautiful again. I can outline what I want, give it a checklist and make it do the strenuous part of writing them. I don’t even have to remember the cascades.
I like tailwind because it lets you spend more time on things that matter and not bike shedding styles , it just doesn’t matter, zoom out more.
> I got curious about what writing more semantic HTML would feel like.
This is so relatable. In the beginning of my career, I used to add so many dependencies for things I did not know. But these days, I mostly work on removing dependencies because I'm a lot better at using the web platform. I treat the web platform and browser primitives as materials to build what I want rather than a blank canvas to paint things from scratch.
I'd like to think that LLMs help with the first approach, I'm certainly now a little more curious to try plain CSS again.
We actually loved it so much that we’ve taken over maintenance of a fork, and just released our Pico successor candidate: https://blades.ninja/
to be tracked here: https://github.com/anyblades/blades.ninja/issues/7
It scopes CSS to components by default, and keeps HTML, CSS and JavaScript seperate.
<template>
<!-- Largely just HTML -->
</template>
<script setup lang="ts">
// JS/TS as you would expect
</script>
<style scoped>
/* Component scoped styles here */
</style>
Very clean, easy to understand, and (as someone who started hand writing DHTML) it still feels very much like DHTML with more convenience and modern affordances.[0] Vue SFC docs: https://vuejs.org/guide/scaling-up/sfc.html
What you want to share/cascade is variables, not styles. Styling components makes it easy to make sure the styling of each component is isolated and doesn’t have unintended cascading effects. When working this way, using Tailwind is as much a good pattern than, say, CSS modules (which I like too).
The creators of Tailwind wrote the brilliant book "Refactoring UI" [0], which presents a systematic design system, introducing ideas such a type-scales, color-scales, spacing, and tactics to minimizes the cognitive burden on the designer by forcing design choices. The ideas presented in this book basically are tailwind classes!
When you build with Tailwind, everyone is speaking the same design language, and you end up with harmonious designs, even when components came from different projects or designers.
I disagree with the author's approach. It's basically just copying the utility classes from Tailwind and implementing them in an unergonomic way. Perhaps the best idea is component level CSS which is something that's been enabled by better tooling. I would implore anyone who doesn't really get Tailwind, to read the origin story [1].
[0] https://refactoringui.com/ [1] https://adamwathan.me/css-utility-classes-and-separation-of-...
Atomic CSS really solves problems that only exist if you're holding the tool wrong, in my humble hot take.
@import 'tailwindcss';
p {
@apply text-justify;
@apply bg-slate-300 dark:bg-slate-800; /* Second rule just for colors */
display: block; /* regular CSS */
}
I used to be a big Tailwind hater because putting all those utility classes as inline styling into my HTML is a crime against nature. But this way I get the best of both worlds. Tailwind is really nice as higher-level building blocks and saves me from writing a bunch of media queries.It’s really not when working with components instead of pages, and when working with variables properly
- Engineers never learn to properly use developer tools to debug CSS
- Components get gigantic bloated piles of classes that are not human readable
- Those gigantic piles of classes get logic in them, that often would have been easier to write as a CSS selector. Tailwind developers learn to write a JS ternary operator with a string of classes instead of ever learning how CSS selectors work
- Those ternary operators get too complicated. The engineers write object maps of Tailwind classes, or export consts of strings of Tailwind classes to use later. Those object keys and const names are what the CSS class names could have been if they just used CSS. They literally re-invent CSS classes, but worse.
- Tailwind classes can't be migrated. You can migrate CSS to Sass to CSS modules to Emotion CSS to etc mostly just by copying them over, because all of those are CSS (with some quirks). Tailwind classes are non-transferable
The happiest medium I've found was in an organisation of around 200 UI engineers: scoped CSS so that engineers can work with autonomy without colliding with other engineers, plus Tailwind for quick band-aid fixes.
Tailwind classes are literally vanilla CSS classes. You can copy-paste their definitions directly
I don’t believe Tailwind is inherently worse than pure CSS. If Tailwind had existed from day 1 on the web and you had learned it first you probably wouldn’t say this. In fact, if Tailwind had existed first somehow, and someone came up with CSS as we know it as a new revolutionary library, I’m not sure it would have succeeded.
I still like it though. it’s one of those abstractions that actually helped me learn. I would go to the tailwind doc pages and see the underlying css of any class.
There were some other frameworks I got excited about: vanilla extract and stitches, both made by some really talented people. I wonder why those never quite got the same traction…
https://docs.google.com/document/d/1oIb025h3UcHMJA1zPtk1kauo...
The HTML bloat was really tough to deal with. I spend far more time in HTML than I'd like, and having more Tailwind classes than I do semantic HTML was really tough to look at.
I've settled on using vanilla CSS and applying styles per-page on an as needed basis. For example, include base styles (reset, primary theme, etc) and then include marketing styles (or: blog styles, dashboard styles, syntax highlighting styles, charting styles, etc).
It keeps each page light and minimal. Reading the HTML is easy. Styles stay consistent across any pages that share styles, etc.
Just using tailwind and anchoring around a design system like shadcn is just way easier for a team to align around than somebodys made up css language.
> Builders value getting the work done as quickly and efficiently as possible. They are making something—likely something with parts beyond the frontend—and are often eager to see it through to completion. This means Builders may prize that initial execution over other long-term factors.
> Crafters are more likely to value long-term factors like ease of maintainability, legibility, and accessibility, and may not consider the project finished until those have also been accounted for.
> In my view, the more you optimize for building quickly, the more you optimize for homogeneity.
We actually loved it so much that we've taken over maintenance for a fork here: https://github.com/anyblades/pico
My favorite is when colleague A broke something from colleague B, who fixed it but broke sometimes from me, and I fixed that and broke what colleague A did. The process repeated once more and it landed again on my desk, where I said wait a minute, I've been here already. We were than able to fix all three things at the same time.
So it's difficult to keep track of everything.
<button class="bg-blue-600 text-white px-4 py-2 rounded">
With style: <button style="background-color: var(--color-blue-600); color: white; padding: 8px 16px; border-radius: 4px; border: none;">
Now more interestingly, Tailwind with hover and focus styles: <button class="bg-blue-600 hover:bg-blue-500 active:bg-blue-700
text-white px-4 py-2 rounded transition-colors
focus:outline-none focus:ring-2 focus:ring-blue-400">
That’s not possible with the style attribute.Even more interesting with Tailwind, a div with dark mode and responsive styles:
<div class="
bg-white dark:bg-zinc-900
p-4 md:p-8
rounded-xl
shadow-sm dark:shadow-none
border border-zinc-200 dark:border-zinc-700
">
That’s not possible either with the style attribute.Now your first instinct might be to "that’s unreadable", but keep in mind HOW you actually read and write this code. You’re not actually reading it to understand what it does like you do with iterative code. You see how the browser renders it, and you just adapt the code. Tailwind code is mostly write-only and maintained by viewing what the component looks like. This code doesn’t need to be reusable either, the whole component needs to be. The Tailwind code inside is unique.
E.g. how do you style a child on parent hover with the style attribute?
How I refactored my rats nest of CSS back to tailwind.
In a work setting, beware of DIY Syndrome. Move away from an established tool, and now your on the hook for documentation, onboarding and maintenance. When the one dev who cares about that leaves the company, everything will fester. It never ends well.
next thread
Separating HTML and CSS into different files is just like separating a bunch of methods/functions into different files, or splitting one monorepo into git submodules. Yeah, it sometimes makes sense, but if you're doing it for the sake of separating things then just stop.
I think the only point of Tailwind is to make front end devs realizing how much separation of concerns is misunderstood and misused as a dogma. Once you realize that you can ditch Tailwind if you like.
What you see in reader mode is the content layer. HTML is a part of the presentation layer.
Nobody cares about true REST (modern day RESTful is a different thing), HATEOAS or semantic web. People tends to simplify things.
We probably can live with just 7 html tags,<title>, <style>, <div>, <form>, <input>, <button>, <a> and CSS.
Frameworks like React that add structure to the data flow, component encapsulation, and a huge repertoire of patterns to train on, plus Typescript for immediate compile-time feedback loops… those are what LLMs thrive on.