Semantic vs utility classes is a religious war that will outlive us all, so I'll try to stick to my personal experience and not get too far into the weeds.
With that said: this resonates strongly with my experience using utility classes (Bootstrap, not Tailwind, but I think it's close enough for these purposes). On the giant front-end codebase that I used to manage - which didn't use any CSS libraries or utility classes - here are some things I learned:
- It's okay to have semantic classes and also bundle commonly repeated styles under reusable classes. Most of our classes were component-specific, but we had maybe four or five cases where a particular set of multiple(!) CSS properties were repeated in all sorts of different semantic contexts, so we encapsulated those as "utility" classes. However, when it was just one property getting used frequently...
- We learned that inline styles aren't the devil. It's perfectly fine to do `style="display:flex; margin-left:1em;"`. The only disadvantage this has compared to utility classes is brevity. And in exchange, it's much more readable and carries zero bloat or lock-in. The verbosity also encourages you to move styles to a more meaningful place once the amount reaches a certain threshold.
- For consistency in styling/spacing/branding: just use CSS variables. Preprocessor variables work fine, as do native custom-properties. `margin-left: var(--m-4);` works just as well as `class="ml-4"`, with the added benefit that it can be used equally well inline or in a stylesheet (and, again, no bloat or lock-in). Another interesting benefit is that that value can be reused and assigned to other properties; for example, maybe in one spot you want to `padding-left: var(--m-4);`.
- Unpopular opinion: your HTML and CSS aren't really separate concerns in most cases. Sometimes you can find repetition that can be factored out, and if so that's great, but the CSS that controls your layout is and always will be intimately tied to the DOM structure it's targeting. Given this, in my experience, you're much better off having hyper-specific class names (most of the time!) and, if you find it helpful, some sort of componentization/scoping system to make sure they stay hyper-specific and don't leak (I didn't find that we needed the latter, though only because we followed very strong conventions).
That's just my two cents. In my decade of writing CSS I've come out against utility classes as a formal methodology, though I'm also very comfortable hand-writing CSS and I've mostly worked at small companies, so those factors and others may bias my opinion.
With that said: this resonates strongly with my experience using utility classes (Bootstrap, not Tailwind, but I think it's close enough for these purposes). On the giant front-end codebase that I used to manage - which didn't use any CSS libraries or utility classes - here are some things I learned:
- It's okay to have semantic classes and also bundle commonly repeated styles under reusable classes. Most of our classes were component-specific, but we had maybe four or five cases where a particular set of multiple(!) CSS properties were repeated in all sorts of different semantic contexts, so we encapsulated those as "utility" classes. However, when it was just one property getting used frequently...
- We learned that inline styles aren't the devil. It's perfectly fine to do `style="display:flex; margin-left:1em;"`. The only disadvantage this has compared to utility classes is brevity. And in exchange, it's much more readable and carries zero bloat or lock-in. The verbosity also encourages you to move styles to a more meaningful place once the amount reaches a certain threshold.
- For consistency in styling/spacing/branding: just use CSS variables. Preprocessor variables work fine, as do native custom-properties. `margin-left: var(--m-4);` works just as well as `class="ml-4"`, with the added benefit that it can be used equally well inline or in a stylesheet (and, again, no bloat or lock-in). Another interesting benefit is that that value can be reused and assigned to other properties; for example, maybe in one spot you want to `padding-left: var(--m-4);`.
- Unpopular opinion: your HTML and CSS aren't really separate concerns in most cases. Sometimes you can find repetition that can be factored out, and if so that's great, but the CSS that controls your layout is and always will be intimately tied to the DOM structure it's targeting. Given this, in my experience, you're much better off having hyper-specific class names (most of the time!) and, if you find it helpful, some sort of componentization/scoping system to make sure they stay hyper-specific and don't leak (I didn't find that we needed the latter, though only because we followed very strong conventions).
That's just my two cents. In my decade of writing CSS I've come out against utility classes as a formal methodology, though I'm also very comfortable hand-writing CSS and I've mostly worked at small companies, so those factors and others may bias my opinion.