1. 64
    1. 30

      Tailwind & consort kinda target something the author seems to forget: many web developers of today are deeply entrenched in “component based frameworks” that kinda prevent such repeat of “atomic css snippets” since, well, you develop a “component” that get repeated as needed.

      Classic class based CSS already do this, ofc, but only for CSS + HTML, when you try to bring this component/class system to Javascript, you often ends up in something like React or Vue or idk what’s trendy today.

      And then you have 2 competing “components” systems : CSS classes, and “JS components”.

      Tailwind kinda allow you to merge them again, reducing overhead when having to work with those JS systems.

      1. 6

        I personally prefer the CSS Modules approach to solve this problem, rather than bringing in a whole framework like Tailwind. CSS Modules give you the isolation of components but don’t force you to leave CSS itself behind, and when compiled correctly can result in a much smaller CSS payload than something hand-written. Sure, you lose the descriptive and semantically-relevant class names, but if you’re building your whole app with components, you really don’t need them.

        That said, if I didn’t use something like React, or I just needed CSS that followed a similar modular approach, I guess I would reach for Tailwind. But realistically, CSS is so lovely these days that you really don’t need much of a framework at all.

        1. 3

          I find tailwind much easier to use than css modules when you stick to the defaults.

        2. 3

          CSS Modules is an abstraction at a lower level than Tailwind. The former can do everything Tailwind can do in terms of the end result. The latter provides really nice defaults/design tokens/opinions.

          1. 2

            CSS Modules is an abstraction at a lower level than Tailwind.

            Definitely, and that’s why I prefer it. The way I normally organize my applications is by using components, so I don’t really need a whole system for keeping all the styles in order. Ensuring that styles don’t conflict with one another when defined in different components is enough for me. But if I was doing something with just HTML and I didn’t have the power of a component-driven web framework, I probably would try out Tailwind or something like it.

    2. 20

      I long left web development. Funny to see that this Tailwind thing seems to rediscover approaches we already overcame 10-20 years ago in web development when we went from hard-coded styles to CSS classes. And now Tailwind goes a step backwards and uses almost-hardcoded styles again disguised as CSS classes.

      I also love this quote, which according to the article comes from Tailwind:

      You have to think up class names all the time — nothing will slow you down or drain your energy like coming up with a class name for something that doesn’t deserve to be named.

      Yeah, naming is hard, let’s stop naming things so nobody who reads the code later understands our intentions :)

      1. 10

        Yeah, naming is hard, let’s stop naming things so nobody who reads the code later understands our intentions :)

        This is a straw man of the argument, which is not that you should avoid all names, but that you should avoid names when they aren’t needed. It is the difference between (part of) McIlroy’s famous UNIX 1-liner:

        tr A-Z a-z |
        sort |
        uniq -c |
        sort -rn |
        sed ${1}q

        and something like this (pseudocode):

        lowercased = lowercase(input)
        sorted = sort(lowercased)
        counts = uniq_counts(sorted)
        sorted_down_by_count = sortByCount(counts)
        topN = topN(sorted_down_by_count, n)

        The only thing we care about naming here is the final result: “the top n most frequent words”. Naming the intermediary calculations within this context is just noise, and makes the code less readable, not more.

        1. 9

          The only thing we care about naming here is the final result: “the top n most frequent words”. Naming the intermediary calculations within this context is just noise, and makes the code less readable, not more.

          Someday, in one, two, twenty years, somebody will go through that code to debug a problem with locale-aware sorting and they will thank you for having named the intermediate steps. Or they will curse you having named only the result.

          1. 5

            they will thank you for having named the intermediate steps

            They won’t, because the intermediary names provide no new information.

            1. 9

              the intermediary names provide no new information.

              I would agree with this if the intermediate commands used descriptive long options, like sort --reverse --numeric-sort. In that case, yeah, sorted_down_by_count doesn’t add much. But compared to sort -rn and having to guess what those short options do or needing to peek at the manpage, i’d take a meaningful name like sorted_down_by_count any day.

              Short options are nice for exploration on the command-line. But for actual scripts written onto files that other people might need to read, the descriptive naming of long options should always be preferred IMO.

              1. 4

                Yes I agree with this point. I was just using the McIlroy command verbatim. But long options for scripts all the way.

            2. 3

              They absolutely do provide new information

              Firstly, you don’t have to mentally translate tr A-Z a-z into ‘start by lowercasing the input’ every time you read the code: you directly read ‘lowercase the input’. So the name provides information that is new when you haven’t mentally performed the translation step yet. Which you then don’t have to, because you already have the name.

              Secondly, the name communicates the intent: the ‘sorted’ variable is intended to hold a sorted list. If some refactoring or other code shuffle results in that variable being used before ‘sort’ is called, that’s a clear red flag to the reader that something went wrong. So the name provides additional information that the first version does not have at all. You could use documentation/comments instead of names, but not the plain McIlroy version.

              I’ll take the second version over the first one for anything that isn’t a throwaway command I construct just to get something done. I’m not a shell and I don’t like being forced to play one.

              1. 4

                I used to think like this too, and the argument sounds reasonable, but it leads to an overly verbose style of programming which hinders readability for anyone fluent in the language.

                The only situation where the argument holds water is if you expect your maintainers to be unfamiliar or barely familiar with the language. Sometimes this does happen, and turning your code into a mini language tutorial can be appropriate then.

                Generally, there are two kinds of errors you can make (in code or prose): you can under-explain, or you can over-explain. It seems to me the position you are arguing assumes that over-explaining is not really an error, or is only a minor one. But this is not the case. Habitual over-explaining wastes a lot of time and greatly dampens the efficiency of communication. You want to get it just right.

                Firstly, you don’t have to mentally translate tr A-Z a-z I’m not a shell and I don’t like being forced to play one.

                Maybe you aren’t that familiar with bash? Because if you are, it is faster to read the original version. You immediately recognize the familiar names and what they mean.

                In fact, the new names introduce new cognitive load, because I must now load up the bespoke lexicon a random programmer into short term memory, mapping between the new terms and their definitions, which I already knew. And no matter how good you are at naming, your “self explanatory” names will never be as good as the terms someone already knows by heart.

                1. 2

                  I used to think like this too, and the argument sounds reasonable, but it leads to an overly verbose style of programming which hinders readability for anyone fluent in the language.

                  This sentiment is often stated, but I don’t think it’s true.

                  People read/consume entire words at a time. For example, “–reverse” may be more verbose than “-r”, but it requires the same cognitive effort for experienced veterans. (And, obviously, a lot more cognitive effort for newbies.)

                  This is why APL isn’t popular, and it’s also why glyph-based written languages are, in my opinion, objectively inferior.

                  1. 3

                    This is why APL isn’t popular, and it’s also why glyph-based written languages are, in my opinion, objectively inferior.

                    As a counterpoint, and one of the best demonstrations of what I’m arguing, I encourage you to see whether the source or rendered output of Latex is easier for you to read:


                    In the case of APL, the confounding variable is that acquiring fluency takes a very long time. But as someone fluent in J, and somewhat familiar with APL, I can attest that it can become quite readable. Aaron Hsu has some good videos on this.

                    For example, “–reverse” may be more verbose than “-r”,

                    As I said in my other response, I am in favor of long form flags, but it is a fair question to ask me why, as my argument could imply that short ones are better, so long as they are familiar. I think that, with flags, that so long as they are familiar is just too rarely true – there are too many of them, across too many tools. It’s too hard to make assumptions about familiarity, as opposed to “knowing what sort or tr does”, which is safe to assume of a bash programmer.

                    There is a lot of judgment involved in getting your “density” right, and I am not arguing to never have intermediate names, far from it. Only that there are plenty of times where doing so is a mistake.

                    Steve Yegge’s “Portrait of a n00b” is relevant here too:

                    The question is, what do you do when the two groups (vets and n00bs) need to share code?

                    I’ve heard (and even made) the argument that you should write for the lowest common denominator of programmers. If you write code that newer programmers can’t understand, then you’re hurting everyone’s productivity and chances for success, or so the argument goes.

                    However, I can now finally also see things from the veteran point of view. A programmer with a high tolerance for compression is actually hindered by a screenful of storytelling. Why? Because in order to understand a code base you need to be able to pack as much of it as possible into your head. If it’s a complicated algorithm, a veteran programmer wants to see the whole thing on the screen, which means reducing the number of blank lines and inline comments – especially comments that simply reiterate what the code is doing. This is exactly the opposite of what a n00b programmer wants. n00bs want to focus on one statement or expression at a time, moving all the code around it out of view so they can concentrate, fer cryin’ out loud.

                    1. 2

                      There is a lot of judgment involved in getting your “density” right


                      And there’s probably no “one true density” because people are different, even people with the same level of experience (e.g., veterans in computer programming language X).

                      My real goal here is simply to express the idea that, for example, “–reverse” does not hurt, or slow down, veterans, when reading – their brains gulp down the entire word “reverse” in a single “gulp” - at least, based on what I’ve read about how the human brain works and reads.

                    2. 1

                      I encourage you to see whether the source or rendered output of Latex is easier for you to read:

                      I’d argue that rendering LATeX as glyphs is exactly the same as rendering tr A-Z a-z as lowercase.

                      If you’re the one who wrote the LATeX and revised it umpteen times, you don’t need to look at the rendered version: you just read the LATeX. The rendered version is for other people to read. And those are not people unfamiliar with LATeX or noobs. It’s also yourself a few months later: then you prefer the glyphs over the LATeX.

                      1. 1

                        What you’re missing is that the names typically won’t have the familiarity of well-known mathematical symbols. Sure, in the case of “lowercase”, it does. But the point is that what you typically get is programmers inventing strained words for ideas that don’t map to well-known concepts or anything in the domain – because they’re naming “in between” stuff. The result is a name soup that clouds rather than illuminates.

                        Another analogy:

                        The phrase “jump in place” will usefully convey to you a sequence of body positions and the associated motion.

                        But if I am inventing some weird new dance move that you’ve never seen, which is used only once, then naming it is a waste of time. The demonstration of the move is the most efficient way to communicate it.

                2. 1

                  Because if you are, it is faster to read the original version. You immediately recognize the familiar names and what they mean.

                  I have my doubts about that, primarily because that’s just not how it works for me.

                  It only happens like you describe when it’s a piece of code I have recently seen and I already know what it says. Then the pattern tr A-Z a-z is just a place locator for where I am in the code. I don’t need to interpret it, because 9/10 times I don’t care for it. I already know the code is correct, because I’ve recently seen it.

                  If it’s new code or it’s been a few months, then I need to doublecheck. What happens in my head is tr, OK, A-Z, OK, a-z, OK, we’re lowercasing everything. Every time. It may look like tr A-Z a-z at a glance, but I have to check. It looks a lot like tr a-z A-Z, tr A a, tr AZ az, etc. There are too many small variants that change the meaning. ‘Lowercase’ does not have that problem and has the advantages I described before. The word carries more information than the command.

                  1. 1

                    Now the conversation is getting interesting, because I agree with a lot of what you just said but still, for most use cases, prefer the original code.

                    because that’s just not how it works for me What happens in my head is tr, OK, A-Z, OK, a-z, OK…. ‘Lowercase’ does not have that problem

                    This is a fair point, but I have two counterpoints:

                    1. The “checking” you describe is really fast, like a second or less. Granted I have written a lot of bash, but fluency is one of my assumptions.
                    2. This is perhaps the more important point…. You either:
                      1. Have to “trust” that the name “lowercase” is correct (just as you’d trust that it’s A-Z a-z and not a-z A-Z when scanning without “checking”)
                      2. Or have to read the definition anyway.

                    So in either case, you are choosing your reading mode: “grok gist and trust details” or “verify code” (mode typically depends on use case – eg, if you are debugging you need to verify). If you are “verifying” then the advantage of “lowercase” doesn’t help you (you still have to verify), and if you are “grokking the gist” it also doesn’t help you, because all you care about is that the entire phrase “gets words sorted down by frequency of use”. It only helps when you are unfamiliar enough with the language that, say, it saves you having to read documentation to figure out what a function does.

                    There’s more…. I chose the example because it was a brief and convenient way to get my point across, but it’s actually not great at demonstrating the worst parts of this style of intermediate naming….

                    1. The names I chose here are pretty clear, which is “lucky” in a way, because the intermediate objects map to familiar ideas with well-known descriptions. In general, the intermediate objects of your data transformations don’t, and you end up groping to name things that don’t really have “names” (“user input with names validated but addresses still unvalidated”, etc).
                    2. If you do this anywhere but in a tight local scope (eg, within a small function), the names pollute the larger scope, and a reader must verify that “this is just a temporary name used to describe a step in a data validation” and not “a variable definition that we use again later.”

                    Again, I’m not against this style when it’s really needed to clarify something confusing… but only against the idea that “all familiar language constructs / short code idioms are inherently more confusing than a named variable.” That view, to me, is almost akin to code like the following, where every line has a redundant comment, which I’m guessing you do not like:

                    // Gets the array without the first element
                    const tail = arr.slice(1)
        2. 4

          Yes, and as the article mentions, CSS lets you avoid names when they’re not needed.

      2. 7

        if you’re talking about moving away from hard-coded style attributes, this isn’t really a fair comparison at all

        There are 2 main differences as I see it

        1 - tailwind classes are reusable & concise both as utility classes & via components

        2 - you can’t do :hover or :active states among other more complicated css features in style attributes

        I think like everything its hard to judge at a distance, need to really try it out to see if its actually problematic.

        For me, I used it for a few years & then I stopped but I’m seriously considering coming back because it tends to be better css than I write. I think its really great if you really know css, but for me it was valuable to spend a lot of time in plain css to understand it better.

      3. 5

        Web development was really different 10-20 years ago. I would argue that Tailwind isn’t really the same approach that we overcame but rather a familiar approach in a different context (component-based frameworks and libraries).

      4. 2


        Yes. I was working on a series of posts about “naming things” this summer, and in the last two posts (https://t-ravis.com/post/doc/the_gizmos_role_in_markup/, https://t-ravis.com/post/doc/the_trouble_with_anonymous_gizmos/) I was bumping up against this and briefly included Tailwind in one of my drafts.

        I (temporarily) backed down from mentioning it when I caught a glimpse of truth underlying the separation of concerns part of the argument. I decided to take some time to reflect on whether it’s a logical reaction to (and makes lemonade of) a cluster of ways that HTML+CSS muddle separation of concerns (which I was/am already planning to write about).

    3. 10

      I have mixed feelings about Tailwind.

      On one hand, I love it and use it in all my personal projects. I’m so productive with it, and it’s a real pleasure to use it.

      On the other hand, I will probably never suggest using it in a professional project. It’s too hard to defend it, and probably not worth the debate time.

      1. 10

        On the other hand, I will probably never suggest using it in a professional project. It’s too hard to defend it, and probably not worth the debate time.

        We started using it at work, and people that aren’t particularly front end developers end up loving it because of its productivity. That alone is worth it for the business case.

      2. 3

        Yes agreed. I have been using it in my LiveView app. But, when I explain it to my friend who is a front end developer he always just says, “but you can do all that with regular css and JavaScript (in relation to LiveView)”. And all I think is, “yes, but I don’t want to.”

      3. 1

        I hear you. I use it everywhere. I can’t suggest or even recommend it in a professional project—too many pieces of the puzzle for coherent UI are missing: the string-based nature of a styling system based entirely on classNames is hard to override in component trees and difficult to make type-safe

        Chakra-UI is pretty good and pretty similar to Tailwind at least as far as design tokens go, however I could not recommend Chakra because of their willingness to break compatibility with React 17 at the drop of a hat, with no warning or deprecation strategy, while React 18 was barely out of the oven.

        In my personal projects, my current approach is to build basic components with React-ARIA headless component library and styling them with Tailwind. It is really nice.

      4. 1

        I use the style attribute in my personal projects. For me, Tailwind does basically the same but with yet another syntax. If it gets more complex than what “style” can handle, it deserves a class name.

    4. 6

      I’m not a frontend guy, and I’m not really interested in learning all the ins and outs of CSS. Frontend people may hate my guts with the fire of a thousand suns for saying this, but Tailwind’s approach actually works out pretty well for me. They capture all the knowledge of how to do stuff with CSS and give me a uniform interface language of just lego-block class names to put together in my UI. It’s hard to beat that for the busy developer.

    5. 5

      I am not disagreeing, but I find that a Tailwind stylesheet makes it possible to maintain multiple apps with mostly consistent style. You just pick the components from a library, slap it into a template, tweak with utility classes and call it a day. No need to fiddle with CSS and sync it across the projects. Popular components get included into the library.

      It’s super messy and far from ideal, but if you’re on a budget, it makes it possible to get things done with minimal communication overhead while still maintaining a semblance of a corporate identity.

      Tailwind is a poor fit for a standalone application / product, I believe. Bootstrap or something more minimal (just for the grid) and custom CSS would work better.

      I do hate the gigantic compiler, though. Someone should rewrite it in Rust. :-)

      1. 2

        I agree, but for the reason that Tailwind doesn’t really have a solution to the problem of overriding styles within component trees, which seems somewhat inevitable when developing applications with a lot of UI.

        Not such a big problem for smaller applications. One option is to just pass in a className prop and clobber a component’s default styling. That doesn’t work so well in larger applications—you often want to keep some existing styles but override one or two because of how a component needs to look in a different context.

        Maybe more upfront planning and better UI architecture would obviate such a problem. And yet, at the end of the day, sometimes you need to just override that one damned property, submit the PR, and go home.

        The article seems way too sure of its answers for me to take it seriously.

    6. 5

      “I don’t like it so it’s an anti-pattern”

      I don’t like Tailwind’s approach (I’m more of a CSS modules fan), but it seems to work reasonably well and people that use it seem to really like it. That makes it a different approach with its own pros and cons, not an anti-pattern.

      1. 3

        I mean they lay out some actual issues with the framework–beyond not liking it. It might be a stretch to call it an anti-pattern, but if the goal is to click bait, but actually make people second guess their views of ‘best practices’, then it did its job.

    7. 4

      I use Tailwind* on most of my personal projects. Most of them also happen to be built with React.

      I don’t think I would use Tailwind if I wasn’t using it with some kind of component framework. It’s hard to take this article too seriously given that it doesn’t mention how many of its points are obviated by using Tailwind within that context.

      *I’ve stopped using Tailwind directly, and now use UnoCSS. UnoCSS allows me to use the Tailwind classnames that I am familiar with, but is faster with a nice set of additional features that help cut down on cruft:

      <div class="hover:(bg-gray-400 font-medium) font-(light mono)"/>

      transforms to:

      <div class="hover:bg-gray-400 hover:font-medium font-light font-mono"/>

      Can also declare shortcuts in a configuration file like:

      shortcuts: {
          'btn': 'py-2 px-4 font-semibold rounded-lg shadow-md',
          'btn-green': 'text-white bg-green-500 hover:bg-green-700',
      1. 1

        <div class="hover:(bg-gray-400 font-medium) font-(light mono)"/>

        Okay, but what does this div do? Without a semantic tag or a class name, the developer will have no idea why it’s there.

        1. 4

          Given that the point the parent comment is making is that Tailwind is generally used within the context a component framework, that should be made pretty clear by the name of the component.

          Really, I think many of the commenters in this thread could do with reading up on the rationale behind Tailwind. There are some great resources out there, but if you don’t have the problems Tailwind aims to solve, then it’s hard to understand why some people are so passionate about it. I’m sure there are plenty of cases where Tailwind is overkill, but that doesn’t make it an antipattern. If you’re going to criticise something, at least try to understand the motivation behind it.

          I was sceptical myself at first. I even tried it early on and it didn’t stick. But I kept running into the same problems, so I determined to give it an honest chance and it has been an absolute joy to work with.

          It’s the first time in 15+ years of web development where I haven’t had to constantly think about how to structure things, which class naming convention to use, how to name a ‘container/wrapper/inner’ class, keep track of which CSS is in use and where, or deal with CSS specificity issues.

          But the best part, is that it gives you flexibility with limitations. These limitations are what make it trivial to maintain a consistent design system, and it’s up to you do decide where those limitations are. The defaults will take you a long way, but if/when you need to change something (spacing, colours, fonts, anything…), it’s usually just a few lines in the config file.

    8. 4

      Overall, this is my experience too. Folks that don’t do much front-end find CSS too burdensome, but I find ugly markup harder to work with over time (e.g. stacks of utility classes that really aren’t very flexible). There’s a lot of value in thinking about how that cascade is gonna cascade, and how your CSS variables can float down into them which can lead to some very minimal CSS and HTML.

      I do still think there’s a place for utility classes, but it should be used more sparingly than not. Every Layout has a common set of patterns you’ll often have to do, but instead of what our author does, I find a utility class like <div class="Stack Stack--wideGap"> to be more useful than the one class name per property (OR depending just letting the variables do their thing <div class="Stack FooComponent"> with .Stack { display: flex; gap: var(--Stack-gap, 1.6rem) } .FooComponent { --Stack-gap: 2.4rem }). Additionally, I find that this tends to scale better relying on flex and grid than doing owl selectors, > * + * as margin-* is not going to break down on smaller viewports as well as leveraging gap–it feels optimized for an older web that may not be worth everyone’s time budget (which if you make your site work on Netsurf, props to you for the compatibility!).

      I agree BEM sucks, but it isn’t an anti-pattern, it’s just really, really hard to read compared to basically all other naming schemes. The nice thing about it is that you can adjust your markup a little better. Did your junior dev think that navigations necessarily need to be in an <li>? Well, if it’s classed <li class="Navigation-item"> I can just slap that on the <a href class="Navigation-item"> and remove the often unnecessary list. Nesting can also get unclear as the project scales and it will be easier to find your class in the CSS file via ripgrep or whatever if you have a searchable name to it. Seeing that we disagree is the takeaway though: we should learn about other systems and use them when they make the most sense instead of taking everything as dogma. If you need a quick <button class="A"><span/></button> and .A > span { } makes the markup cleaner, then by all means, do that.

      1. 5

        Did your junior dev think that navigations necessarily need to be in an <li>?

        In this case, I think the junior dev is right. Having navigation links in a list is better for accessibility, because some screen readers announce the number of items in the list and let you skip past the list entirely. Granted, it may be enough to define a main landmark so a screen reader user can skip the navigation header altogether, but in general, the more tools a blind user has for skimming the page as a sighted user can naturally do with their eyes, the better.

        1. 1

          Skip links help a lot here, “skip to content”—along with the role/landmarks as mentioned. Some big navigational hierarchies might appreciate it, but I’ve seen a lot of cases where it’s not needed and can make the TUI experience worse because there can be a lot of newlines with just a single word on them.

    9. 4

      I had to modify some non-Tailwind BEM SCSS yesterday and I was like, man, this sucks. 😂 I did half assed job because it’s very difficult to maintain compared to Tailwind.

      1. 4

        So true, I can’t fathom all these people arguing against tailwind. It increases delivery speed so drastically. You use it with components, OBVIOUSLY.

        People would rather maintain two separate files which may or may not interact with eachother? Fuck outta here with that. It’s a terrible approach, and CSS modules are marginally better but still extremely open to abuse.

        IMO the biggest problem with CSS is the C.

        1. 3

          We all know that dynamic global variables are bad in regular programming languages, but when it comes to CSS, people are like “no, you gotta use the dynamic globals. that’s why it’s called dynamic globals style sheets!!” 😆

          BEM is fine for what it is, but to be clear, it’s just trying desperately to add some minimum level of encapsulation by using handmade namespaces. That’s a good thing, but much better is if you can just style the things you want to style and not the whole rest of the universe by accident.

    10. 3

      Holy smokes, what a list of classes. What does it look like? What does the button do? Well, it’s hard to figure out. Here’s the same example without Tailwind:

      This does not go on to show the same thing without Tailwind. The same thing without Tailwind is the HTML plus the exact same ugly CSS properties. So many complaints about Tailwind just skate by this. Doing a PR review of a frontend change is really, really hard because CSS is easy to write but very hard to read. When you complain about the unreadable class soup, you are complaining about the unreadable CSS properties that make a button a button.

      Tailwind is not a competitor to Bootstrap. It’s a competitor to Sass. It’s a way to generate CSS properties quickly by using a shorthand in the HTML.

      1. 2

        Here is the missing CSS from TFA:

        .button-menu-toggle {
          display: inline-flex;
          align-items: center;
          justify-content: center;
          border-radius: 0.375rem;
          background-color: #fff;
          padding: 0.5rem;
          color: #888;
        .button-menu-toggle:hover {
          background-color: #eee;
          color: #777;
          outline: 2px solid transparent;
          outline-offset: 2px;
          box-shadow: white 0 0 0 0 inset, blue 0 0 0 2px inset, black 0 0 0 0;

        This is 17 lines and it’s not even doing any of the responsive design stuff that really makes Tailwind MVP.

      2. 2

        the unreadable CSS properties that make a button a button

        What makes a button a button, complete with all necessary accessibility behavior, is the button HTML tag name. Yes, the crappy default styles provided by browsers mean that it’s desirable to style a button. But those ugly CSS properties should be hidden away in a CSS file, behind a reasonable selector, either button (to style all buttons) or a sensible class name (to style only a particular kind of button).

        1. 4

          I thought about rewording “make a button a button” to “look like a button” but I was typing on my phone, oh well.

          those ugly CSS properties should be hidden away in a CSS file,

          Why? Moving the CSS away from the HTML makes it harder to look at the HTML and know why an element is there and what’s it doing. With Tailwind, if there’s a div that exists only to create a context for some other absolutely positioned element, you can see it because the HTML looks like <div class="relative">. You don’t have to toggle between two files to get a sense of what’s going on. In general, Tailwind gives you context for your HTML.

          Here’s some real HTML I have open in my editor now:

              class="bg-black md:py-6"
              aria-label="main navigation"
              <div class="flex flex-wrap items-center w-full max-w-6xl px-3 mx-auto mb-6">
                <div class="flex items-center w-full md:w-1/2">

          I can see from this HTML that nav is there for semantic reasons and has a black background. On desktop plus, it gets some padding. The first div creates a flexbox container for the navbar and the second one is a flexbox subcontainer that is full width on mobile and half width on desktop. The alternative CSS is much harder to read!

          .navbar {
            background-color: #000;
          @media (min-width: 850px) {
            .navbar {
              padding-top: 1.5rem;
              padding-bottom: 1.5rem;
          .navbar__wrapper {
            margin-left: auto;
            margin-right: auto;
            margin-bottom: 1.5rem;
            display: flex;
            width: 100%;
            max-width: 72rem;
            flex-wrap: wrap;
            align-items: center;
            padding-left: 0.75rem;
            padding-right: 0.75rem;
          .navbar__other_wrapper {
            display: flex;
            width: 100%;
            align-items: center;
          @media (min-width: 850px) {
            .navbar__other_wrapper {
              width: 50%;

          This is 35 lines of code vs 3!!

          In an ideal world, Tailwind would run through your CSS and turn long sequences of classnames into single, short optimized names (although I think is an NP hard SAT optimization problem in general). In practice, the HTML bloat doesn’t affect download times because the files always end up pretty small anyway. People are worried about optimizing a non-problem for aesthetic reasons.

          behind a reasonable selector, either button (to style all buttons)

          This ruins codebases. It is literally the single anti-pattern that makes CSS so hard to use in so many cases. It creates spooky action at a distance. Why is this link a different color? Where is this hover effect coming from? Why is my headline too large? How come there’s an extra margin between these elements? It creates an endless supply of headaches.

          You should style bare elements in only two cases: you’re creating a global reset to sane defaults or you’re working with a rich text content well and need to style the bare <p> <strong> <h3> etc. tags in that content well. Never use bare tags under any other circumstance if you value sanity.

    11. 1

      Hot take from someone that rarely does web programming these days.

      Tailwind is going back to pre-CSS days where all styles was inlined all over the HTML content, but with a richer set of styles. This is popular for exactly the same reasons pre-CSS styling was popular: you throw all structure away so (1) your HTML code looks like shit, but (2) it is easy to write, and (3) it is easy to change locally without changing everything else. I suppose that it is easy for non-engineer people to work with the page, they just tweak stuff all over until it looks like they want to.

      When you try to use CSS “properly” instead, you can expose a nicer structure, factor things, it is easier to make everything consistent thanks to a small number of abstractions, but do people actually (a) have the skills to do this and (b) want to? Maybe web design is more naturally an affair of tweaking margins here or there until it looks right, and people don’t want their tweaks to affect other parts of the page that had the same structure?

      Also: CSS used to be a fairly bad language (no variables!), so people are used to preprocessing it in various complex ways. Doing everything with the JS side allows you to regain some structure/sharing on top of a Tailwind approach, so why bother using CSS “properly”?

      My summary: I think that Tailwind is a way to write ugly code that gets the job done, in an area where most people don’t actually care about code quality. I understand why people use it, my programmer’s intuition is that it’s a really bad idea, I don’t want to touch it, let’s let other people write this mess.

      Questions: Who is paying the costs incurred by those noCSS approaches? Don’t designers find it odd when they end up having to changes dozens of HTML snippets all over the place to make a consistent change to the design, and they forget one or two places that they have to fix it later? Maybe they don’t notice because they’re not trained to, and other people don’t see the costs and just pay for time? Do we care about the code quality of a website, when we rewrite it from the ground up every 5 years anyway because the older website looks “old”?

    12. 1

      I don’t love it, but it is not bad either. I ended up using a component library because it is too much trouble to write all the basic component behavior.