Customizing components with Tailwind 3.2.5+

I notice that Phoenix 1.7 uses TailwindCSS 3.2.4, and I notice that they build their components by first giving it some default classes, then you can pass in some additional classes if you want.

For example, you can see this button:

  attr :type, :string, default: nil
  attr :class, :string, default: nil
  attr :rest, :global, include: ~w(disabled form name value)

  slot :inner_block, required: true

  def button(assigns) do
    ~H"""
    <button
      type={@type}
      class={[
        "phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
        "text-sm font-semibold leading-6 text-white active:text-white/80",
        @class
      ]}
      {@rest}
    >
      <%= render_slot(@inner_block) %>
    </button>
    """
  end

Currently, using Tailwind <= 3.2.4, if you wanted the text in a button to be purple instead of white in one particulary instance of this button, you can simple pass it that class like so <.button class="text-purple-500">New User</.button> and it (almost always) applies the classes properly.

In the latest versions of TailwindCSS (from 3.2.5+) the generated css is always deterministic, so whatever the last class defined in the generated CSS file is, that class determines what displays. They say it’s always been wrong to write two conflicting class names on the same element because there’s really no telling how it might ultimately display.

So, I tried upgrading a barebones Phoenix project to Tailwind 3.2.7 then using the included button, and when I add text-purple-500, it no longer “takes”. I add an exclamation point, and it works, because all !classes show up last in the generated stylesheet.

The thing is, they say you want to avoid doubling up on class names of the same type wherever possible, and the “!” should technically be used for edge cases where the class can not be modified, like if it’s a component you don’t control.

So all that to say, what do you think the best way to modify say the core components button that you need multiple colors of but the exact same functionality otherwise? I was thinking of either just adding a get_button_variant_type like this,

attr :type, :string, default: nil
attr :class, :string, default: nil
attr :rest, :global, include: ~w(disabled form name value)
attr :variant, :string, default: "default"

def button(assigns) do
    ~H"""
    <button
      type={@type}
      class={[
        "phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
        @get_button_variant_type(@variant),
        @class
      ]}
      {@rest}
    >
      <%= render_slot(@inner_block) %>
    </button>
    """
  end

defp get_button_variant_type(variant) do 
	 case variant do
      "default" -> "text-sm font-semibold leading-6 text-white active:text-white/80"
      "primary" -> "bg-white text-purple-500 active:text-purple-500/80"
      "clear" -> "bg-transparent border border-black-06"
    end
end

Or giving it a few functions like get_button_bg_color, get_text_color, etc.

There’s also the potential to run everything through something like a utility library like Twix similar to Tailwind Merge that eliminates class names of the same type just leaving the last class of each type, but it unfortunately doesn’t yet work with custom tailwind classes that you set up in your tailwind config file. It also seems like it would have the potential to slow things down if you’re doing this on almost every element on your page, but I don’t know that from experience.

4 Likes

I like the variant approach. :slight_smile: Or only define base classes and always pass the additional classes that you’d need.

Thanks for the quick reply! That’s what made the most sense to me too, but just wanted to see if there were any better ideas. Appreciate it!

1 Like

Using the variant approach is how I’ve always done it. Styles no matter the variant go directly in the component and then I have a way of getting variant specific styles and apply those like you are doing.

This is fine, unless tailwind provides a better way to do it in later releases, I think this is the best way :smile:

3 Likes

You’ve turned my understanding of Tailwind upside down. I’ve always believed that the sequence of mentioned classes is how it works, not the actual location in the output css file but apparently overwriting doesn’t always happen that easy. That explains why Tailwind provides layers into which to inject additional classes.

In a nutshell,

<h1 class="opacity-50 opacity-100">opacity-50 opacity-100</h1>
<h1 class="opacity-100 opacity-50">opacity-100 opacity-50</h1>

end up showing as

image

1 Like