Do you prefer to use Tailwind in your Phoenix projects? (Poll)

This is now the second thread I’ve come back to that has a whole bunch of responses that more or less that what I would have responded.

Yes, and this pertains to sentiments others have said, Tailwind is a constraint based design system (I don’t have a link but easy to look up). It gives me colour and sizes out of the box. Now, it has far more than a general constraint-based system has, but I’m happy with what it comes with and while it’s hard to describe, it’s very easy to stick to the chosen units.

…and this is what I mean about not wanting to get too into debate, lol. I think there is a lot of information out there as to why Tailwind is useful. For context, I’ve been writing CSS since its inception (I’m kinda old), I’ve never hated CSS (I’ve actually always liked it despite its warts) and the first time I saw Tailwind I thought “Yes, that, I want that!” Though again, I enjoy writing vanilla CSS too. It’s sort of a project-dependent thing.

Last thing I will say is that I do find the “This is not what CSS was intended for,” argument a bit disingenuous. People often complaining about updating older tech and older paradigms and yet CSS—one of the most shaky technologies we’re all forced to use—somehow has sacred origins. A ton of stuff has changed since 1998. We didn’t even have components back then.

Crap, I used some em dashes… no I did not write this with AI.

EDIT: Just to expand on @garrison response, having size names with suffixes like -1, -4, -8, etc is a design system convention that far preceeds Tailwind. They are relative. I would also encourage people to read the even the two free chapters or Refactorying UI. It’s quite insightful.

3 Likes

You are preaching to the choir. Even the most diehard tailwind adherents are fully aware that the class syntax is disgusting. The Tailwind marketing page literally used to open with “if you can suppress the urge to retch for long enough…”.

For many, the benefits outweigh the drawbacks. It is completely unproductive to beat this dead horse any further. The path forward is “post a better solution or gtfo”. I have already argued in favor of mine (scoped styles), and I welcome others to do the same.

Lol did you actually try searching for this?

Because it produces more even colors (perceptually), the problem that the Tailwind colors were previously solving (by hand). If you want to learn more just look it up, there are tons of resources on the topic.

Yes of course I know what I’m saying. I was refuting a specific point (that Tailwind is popular due to LLMs) which is obviously untrue. I don’t see how going off on a tangent about ELIZA or whatever has anything to do with my post.

This is unfortunate but my solution is to delete all of that stuff and never use the generators (shrug).

3 Likes

On point 1, if you put your templates in priv/templates/[phx.gen.[live|json|html]/ you can use your own templates for the generators.

3 Likes

Here is a search on main Tailwind page:

Some of terrible results from Google for search Tailwind p:



So yes - I was actually trying to search for this.

1 Like

I did not remember or not know it at all. Does it work if I want to replace just a single file or do I have to copy-paste entire priv directory?

Edit: I read about that already! Just completely forgot it …

If we put one or more customized template files in priv/templates/MIX-TASK-NAME/ then the mix task will look for the file in our project first. Any files we don’t include there will fall back to the Phoenix generator.

Source: Customizing Phoenix Generators · The Phoenix Files

That’s amazing feature! Many thanks for reminding this! :heart:

2 Likes

Fair enough, it was the top result for me but I will acknowledge that search engines ain’t what they used to be!

I suppose everyone else nowadays is using the aforementioned LLMs :slight_smile:

Whatever works for you, but I disagree with that statement.

And how does the Tailwind solve that? Naming scheme is up to you, and with semantic styling you do not have names that can conflict.

And I do not find Tailwind configurable that much. With proper CSS I can make everything super configurable just by using some CSS variables without problem.

That is no problem for me, as I prefer semantic styles in single place, so whole sytem is consistently styled in one place. And I do not know what is so bad about <style>, especially with new @scope rule.

I do not get what is the problem there. I use slots and I do not care how it should be styled, because that is not my concern when I write content.

Why? I can do stuff like oklch(from red l c calc(h + 90)) to change hue by 90 degrees. That allows me to setup whole colour scheme using single variable (main colour) and it will compute all derivatives for me. No need for checking out Adobe Kuler or stuff like that. And Tailwind do not solve that problem either. It picks some colours for you, that is true, but that doesn’t change the fact, that you still need to find proper colour pairings, Tailwind doesn’t fix that. Especially it doesn’t fix the problem of a11y, as the values do not provide you a way to think about contrast in any way.

But the only reference for that scale is itself. I cannot relate it to any other value in my design. It is just floating in the void.

Font size at the root, em is font size for the element. I do not care about exact pixels (especially that CSS px is not 1 physical pixel either), but I care about relations with other components (especially text). Tailwind 1 relates to nothing except of itself for me. Especially when fonts are defined using different units.

That do not change my statement that it seems like designed for it. Still, for me it is attribute styling once again, just with cryptic names.

It never did work properly, as default component will be generated with Tailwind cruft anyway, irrelevant to the provided flags. This mean that I still need to go through whole generated codebase with broom to sweep it from unwanted dust.

And if I need to do that, it reduces the initial usability and just causes irritation.


Another big downside of Tailwind is that it requires compilation step to do tree shaking of styles. Especially when you use custom values like bg-[light-dark(var(--color-white),var(--color-gray-950))]. With “just CSS” I can just place files in repo, and I do not need any bundler or anything. It will just work. I do not need any browser tooling to analyse what does the classes names mean, I can just by hand edit there, and then just copy paste styles from browser dev tools to my project.


I get why people are using Tailwind - they are just lazy, and shushing everything into glorified style= attribute is their way of lazying around. It is simpler to just slap bunch of classes and call it a day. Semantics of my content? What the hell is that? I want to make it pretty and if someone is visually impaired, then it is their problem, not mine.


I simply learned how to live without Phoenix generators, because these always produce more noise than useful code for me. The only generator I use is for Ecto migrations (but it is Ecto generator, not Phoenix) to not need to deal with filenames.

Generators are the weakest and the most irritating part of Phoenix out there. It is just not worth the hassle.

1 Like

You can just put in a single file. So if you just want to edit the index.live you only need to add:priv/templates/phx.gen.live/index.live.

The generators call Mix.Phoenix.generator_paths to get all roots, then use that to call Mix.Phoenix.copy_from which greedily copy a template if it exists at a root. By default generator_paths returns [ “.”, “phoenix”].

1 Like

Yup, updated my post even before your comment. Once again thanks for reminding me this one!

1 Like

I agree, that is why I decided to roll my own flavor of Phoenix. Heavy use of generators to take out all the hassle of adding assocs to forms or changesets and tests. Now everything works nearly out of the box and I can just run all the generators to have a working CRUD app. Adding EctoSync and I have real-time updates propagated to all the sessions.

1 Like

LOL so needlessly combative and generally false. Effin’ up a11y is not a Tailwind-specific problem, it’s a “90% of people who do web design” problem, regardless of the tech they are using. And the lazy argument is needlessly insulting. That’s certainly not how I approach Tailwind-use.

Absolutely. The only ones I use are phx.new, phx.gen.auth, and ecto.gen.migration. I don’t really see a point in using any of the others other than for prototyping. They seem like they are made more with showing different features of LV than something you would actually want.

2 Likes

Because the styles are being specified at the element level and thus there is no need for “class name glue” to track which elements should have which styles, globally, across your entire application and org.

You can then compose at the component level instead for re-use. Components are a better unit of composition because they include the markup, styles, and even business logic and state in some frameworks. It’s better to have everything in one place.

This is a valid criticism but “works out of the box” was the important part there.

The style attribute, not the style element.

Oklch is unambiguously good. I meant that it’s a stretch to say it fulfills the requirements of a design system that works out of the box (because it doesn’t fulfill that at all, actually). But it’s still good!

Tailwind’s colors are indeed an inferior approach, but that’s because they pre-date oklch! That is actually the point I was making, too :slight_smile:

p-1 is equivalent to padding: 0.25rem;. It’s a scale. It’s configurable. I haven’t used Tailwind in a long time but it looks like at some point they rebuilt the whole thing on top of CSS variables or something, so idk how it works anymore.

The lack of a unit is historical. Tailwind is an extension of a “utility class” technique in which you create a standard set of classes (padding, font size, etc) and use those. Pre-generating classes for every imaginable unit would be far too many, so they chose a reasonable scale and stuck with that.

Of course nowadays there is no stripping from a set list (tree-shaking as you described); instead the compiler generates classes on the fly. You can, in fact, write p-[4px] if you want to.

I am not defending this approach, and I have been very clear that I think Tailwind’s time has come and gone. But it’s far more interesting to investigate why things are the way they are instead of just assuming everyone else is stupid and lazy.

The problem I’m referring to there is very specific. Remember that I was talking about CSS nesting.

If you have a component with scoped styles, you want to be able to compose. Like this:

def greeting(assigns) do
  ~H"""
  <style>
    .container { background-color: blue; }
    .greeting   { border: 1px solid red; }
  </style>

  <div class="container">
    <.modal>
      <div class="greeting">Hello, {@name}!</div>
    </.modal>
  </div>
  """
end

We want the CSS to apply to the outer container div, and the inner child greeting div, but not the elements in the <.modal> component (which are elsewhere). This property is actually very important in practice.

Nested CSS is not a useful compilation target for this behavior. I think you are knowledgeable enough that you can probably piece together why without me explaining it in detail. There is actually another CSS feature (the shadow dom stuff) which might work here, but I think it’s actually too restrictive. But that’s another conversation.

HTML and CSS are tightly coupled in practice. This idea that you can write each of them in isolation is a total fantasy. Maybe you can get away with this for something like a blog. But for apps? No.

Just do not use classes for stuff that do not need classes. And from my experience - classes aren’t really needed that often, as you can get pretty far with just good structure and CSS selectors that use it. For example you do not need to set classes for each single one field in form. You can set styles for <input> and go with that. If my application design change, then I do not need to go and update each single component independently, but I have it just there, in plain sight.

At the same time I prefer to not repeat myself over and over again. With Tailwind I need components, because otherwise work with that codebase would be unbearable. What if, hear me out, we could style existing HTML “components” and just use them?

Instead of creating component like:

  def input(assigns) do
    ~H"""
    <div class="fieldset mb-2">
      <label>
        <span :if={@label} class="label mb-1">{@label}</span>
        <input
          type={@type}
          name={@name}
          id={@id}
          value={Phoenix.HTML.Form.normalize_value(@type, @value)}
          class={[
            @class || "w-full input",
            @errors != [] && (@error_class || "input-error")
          ]}
          {@rest}
        />
      </label>
      <.error :for={msg <- @errors}>{msg}</.error>
    </div>
    """
  end

I could just do:

<label>
  Username:
  <input type="text" name="username" />
</label>

Or similar?

No need for fanciness, repeating stuff like class="input" for every single input (it is defined as such, why the hell I need to set separate class for it?).

Then for me the only advantage of Tailwind there over style attribute is that it allows setting some at-rules with it. Other than that it is just worse syntax for the same thing (worse, because it id full of magical names, like p, and with its own magical syntax, like []).

Which requires build step from me. Which goes back to the point - how is that better than using style (which requires no build step) or just styling it on semantic level without any extra classes added to anything?

I never had this problem. If anything I had it other way around. So if we have component:

def foo(attributes) do
  ~H"""
  <div>
    <style>
      @scope {
        :scope { background-color: rebeccapurple }
        .greeting { color: white }
      }
      <div class="greeting">Hello</div>
      <div class="content">{render_slot(@inner_block)}</div>
  </div>
  """
end

How to not apply greeting class to stuff in @inner_block.

Fortunately CSS handle that for us. With simple fix:

def foo(attributes) do
  ~H"""
  <div>
    <style>
      @scope to (.content) {
        :scope { background-color: rebeccapurple }
        .greeting { color: white }
      }
      <div class="greeting">Hello</div>
      <div class="content">{render_slot(@inner_block)}</div>
  </div>
  """
end

Which will now scope the styles only up to .content element.

Of course with that you can also make the .greeting work only for the parent via simple trick:

def foo(attributes) do
  ~H"""
  <div>
    <style>
      @scope to (.content) {
        :scope { background-color: rebeccapurple }
        & > .greeting { color: white }
      }
      <div class="greeting">Hello</div>
      <div class="content">{render_slot(@inner_block)}</div>
  </div>
  """
end

And now even if your code in slot will contain greeting class, it will not be applied there.

1 Like

First, I feel like a few things need to be clarified about Tailwind:

  • Tailwind prefixes however you like: @import "tailwindcss" prefix(tw); <div class="tw:flex">
  • As of v3, it no longer uses tree-shaking. Utilities are only added when detected being used
  • As of v4, it uses native CSS layers, so it’s very amenable to using alongside semantic CSS as a utility class library
  • As of v4, all of the utilities are based on native CSS variables. You can add to these or selectively include what you like. This makes it very easy to leverage in semantic CSS.
  • When searching the documentation, p-4 immediately refers to the padding section as the first result. Using a full class will be much more successful in terms of results.
  • Reverse-searching is also pretty reliable: padding will bring you directly to the padding section as the first result.

The v4 release of Tailwind undid a lot of the weird Tailwind magic, and now it’s a small handful of directives and the rest is normal CSS. As long as you don’t use @apply, it’s actually very easy to migrate a project away from Tailwind simply by copying in the variables and utilities from your last build.

I find Tailwind convenient, if used in a more CSS-centric way than the Tailwind authors recommend. I feel the following value is provided:

  • A standard suite of color, spacing and size variables
  • A standard suite of utility classes
  • A fast, standalone preprocessor
  • Easy customization of built-in themes, utilities, etc.

The value of the above is magnified by the fact the majority of developers are already familiar with these standards.

I definitely agree with the distaste for piling all the utilities into the class attributes. Although that’s what’s recommended, with v4 there’s no real reason to do that just to use Tailwind. Here’s a simplified version of how I like to use Tailwind.

def simple_form(assigns) do
  ~H"""
  <form class={["core-simple-form", @class]}>
    <!-- ... -->
  </form>
  """
end

:root {
  // Synchronize CSS with Tailwind's dark mode -- works with any dark mode strategy
  color-scheme: light;
  @variant dark {
    color-scheme: dark;
  }
}

// Using this layer preserves full functionality of utility class overrides
@layer components {
  .core-simple-form {
    display: grid;
    padding: calc(var(--spacing) * 4);
    background: light-dark(var(--color-zinc-50), var(--color-zinc-950));
  }
}

As mentioned earlier, native CSS @scope or simply very specific selectors can easily be used to handle scoping component selectors from leaking into slots.

Now, I can use the generic form component in a particular place, and have the value of one-off utilities. In particular, breakpoints:

<.simple_form class="lg:grid-cols-2 2xl:grid-cols-3">
  <fieldset class="col-span-full" field={@form[:wide]}>

I have found this to be a very productive way to do things. The reusable components have clean CSS, and I can keep the one-off styling neatly in the component with a handfull of classes.

Regarding co-locating the CSS, the hooks/JS implementation uses a generic MacroComponent. It’s currently undocumented because I think the API and some details are still under revision. Once it’s ready for prime-time, we will be able to easily implement CSS colocation via a MacroComponent of our choosing. Personally, I don’t mind writing my classes for components in a CSS file all that much. I also don’t find much difficulty with naming component classes. Some variant of module + component name is pretty effective for namespacing in any app I’ve worked on (ERP, E-commerce, etc.) That said, a macro component can solve both of those problems in any way the developer sees fit.

I’m not really interested in convincing anyone whether to love/hate use/don’t use Tailwind. It’s a tool, I can work with and without it. Just sharing what I’ve found convenient and useful.

TLDR: We can have both semantic CSS and Tailwind, and it’s actually pretty ergonomic IMO.

6 Likes

This is the fantasy I’m talking about. Okay, you style your inputs globally. Putting aside that even fairly simple apps have different type of inputs, let’s think this through.

My inputs have errors. What do I put them in. The <error> element? Oh that’s right, there isn’t one. So now I’m doing this?

input + ul > li { color: red; }

Great, now maybe somewhere in my app I try to use an input (which happens not to have errors) and a list.

<input type="text ... />
<ul><li>Hello, world!</li></ul>

And now my list is red! Welcome to the Semantic Web.

This idea of the HTML gods defining an element for every little detail did not work out. It did not happen. That’s why we have components. I mean, we also now have WebComponents (sigh) but I am not opening that can of worms rn.

Alright, now do <input class="input-totp-code" />. In real apps we have more than one kind of input, and their behavior transcends element boundaries because there is not an element for everything!

You have reversed cause and effect. Tailwind came after everyone was already using components. It only makes sense in that context.

1 Like

This is essentially the same as the problem I was describing, it’s just the simpler case. I went with the example that demonstrates the problem in both directions.

Yes, @scope is what I was alluding to above. But there are two problems:

  1. It’s not widely available yet, so we can’t use it.
  2. Afaik it scopes inherited properties like text formatting, which I have found (by experience) is the wrong behavior (inheriting formatting across scopes is actually good)

A way around this is to instead use a compiler to extract the <style> contents from each component, mangle the selectors with an attribute, and inject that attribute into each element. If that sounds acceptable to you then we’ve reached consensus.

And there is nothing wrong with using a compiler, btw. I’m already compiling my Phoenix apps anyway. What difference does it make if the compiler touches my CSS too?

What matters to me is whether the CSS looks standard for readers (i.e. in DevTools), and I’ve found the output from this approach to be quite pleasant (and certainly far better than Tailwind’s class hell).

1 Like

I’m pretty sure you have that one backwards: it has the behavior you prefer.

Note: It is important to understand that, while @scope allows you to isolate the application of selectors to specific DOM subtrees, it does not completely isolate the applied styles to within those subtrees. This is most noticeable with inheritance — properties that are inherited by children (for example color or font-family) will still be inherited, beyond any set scope limit.

MDN - @scope

I have not reversed it, because I never stated that. Tailwind comes as a solution to the problem that should not exist in the first place. And it doesn’t make the problem easier to avoid, it makes it even more pronounced. Again Tailwind requires me to use components, because otherwise the styling will fall apart pretty quickly, I never said that Tailwind is what made components popular.

Pretty widely available according to Can I Use. Firefox support is lagging, but otherwise pretty broadly available.

For me this is still a hack, not a solution. Useful hack, but I still prefer to have my CSS separately and use semantic HTML.

Just for example you have modal component there. The question is - why, when we have <dialog> element now, that can implement such feature without all that cruft? You have details component, when we have <details> that we can use. What is the point of anchor component when I can just style <a> element? All of that changes not only will make the codebase IMHO easier to read, but also will help with a11y, as screen readers will be able to inform the user what they are pointing at, without trying to guess from the CSS and behaviour.

Most developers do not understand the importance of the semantic web. And I suspect it’s because the majority of them don’t suffer a disability that would force them to use screen readers. To be honest it’s a lack of empathy that forces them to only consider what is best for them, vs having a user first perspectives.

Yeah it sucks that you have to learn CSS and cascading can be hard since it’s literally the opposite of what they are used to, so the path of least resistance it is.