Button from core_components struggles with class precedence

The default button component generated by Phoenix generators looks something like this at class level:

...
<button
  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
  ]}
>
...

I used the button with an additional color definition to be primary red for example:

<button
  class="inline-flex items-center gap-x-0.5 bg-red-600 hover:bg-red-700"
>

Both classes are nicely appended to the classlist, but this means I now have 2 background colors defined and the precedence of these is not clear and more a thing of luck depending on what tailwind compiled first/last I think.

Am I the only one struggling with this or is the default generated buton not really usefull for this kind of extension?

Core components are part of your application. You can customize them however you need. E.g. you could introduce a higher level attribute :background, which maps or defaults to lower level classes. Concatinating strings of classnames doesn’t really offer any more help for additional semantics like there should just be a single background color.

2 Likes

So it’s considered good practice to change these core components? Had this question for a while

Yes, otherwise they would be in a library not in your codebase. You can even remove them if you don’t intend to use the phoenix generators to generate html pages for you.

2 Likes

I agree with your point @LostKobrakai. The default core_components.ex functions like a starting point and inspiration I guess…

A follow up question then. What libraries already exist for more fully fledged components ready to use as LiveView.Components?
Currently I build my own components based on Tailwind components, but that is only the HTML+CSS layer. The functionality layer overlaps in many applications I dare to guess, hence my question.

Coming from the React community, I am looking for something like MUI but in LiveView terms.

It’s a gap in the ecosystem…

For any future reference and readers, I followed @LostKobrakai’s advice and started extending the default <.button /> component.

It is pretty easy (and fun) to do. Take not it is still very much a WIP

  attr :type, :string, default: nil
  attr :variant, :atom, values: ~w(contained outlined text)a, default: :contained
  attr :color, :atom, values: ~w(primary secondary error black inherit)a, default: :primary
  attr :class, :string, default: nil
  attr :rest, :global, include: ~w(disabled form name value)

  slot :inner_block, required: true

  def button(%{variant: :contained} = assigns) do
    ~H"""
    <button
      type={@type}
      class={[
        "phx-submit-loading:opacity-75 rounded-lg py-2 px-3",
        "inline-flex items-center gap-2",
        "text-sm font-semibold leading-6 text-white active:text-white/80",
        "transition-all",
        @color == :primary && "bg-indigo-600 hover:bg-indigo-500",
        @color == :secondary && "bg-sky-600 hover:bg-sky-500",
        @color == :error && "bg-red-600 hover:bg-red-500",
        @color == :black && "bg-gray-800 hover:bg-gray-700",
        @color == :inherit && "bg-inherit hover:bg-inherit",
        @class
      ]}
      {@rest}
    >
      <%= render_slot(@inner_block) %>
    </button>
    """
  end

  def button(%{variant: :outlined} = assigns) do
    ~H"""
    <button
      type={@type}
      class={[
        "phx-submit-loading:opacity-75 rounded-lg py-2 px-3",
        "inline-flex items-center gap-2",
        "text-sm font-semibold leading-6 active:text-white/80 border",
        "transition-all",
        @color == :primary && "border-indigo-600 text-indigo-600 hover:bg-indigo-50",
        @color == :secondary && "border-sky-600 text-sky-600 hover:bg-sky-50",
        @color == :error && "border-red-600 text-red-600 hover:bg-red-50",
        @color == :black && "border-gray-800 text-gray-800 hover:bg-gray-50",
        @color == :inherit && "border-inherit text-inherit hover:bg-inherit",
        @class
      ]}
      {@rest}
    >
      <%= render_slot(@inner_block) %>
    </button>
    """
  end

  def button(%{variant: :text} = assigns) do
    ~H"""
    <button
      type={@type}
      class={[
        "phx-submit-loading:opacity-75 rounded-lg py-2 px-3",
        "inline-flex items-center gap-2",
        "border-0",
        "text-sm font-semibold leading-6 underline-offset-4 hover:underline",
        "transition-all",
        @color == :primary && "text-indigo-600  decoration-indigo-600",
        @color == :secondary && "text-sky-600  decoration-sky-600",
        @color == :error && "text-red-600 decoratin-red-600",
        @color == :black && "text-gray-800 decoratin-gray-600",
        @color == :inherit && "text-inherit decoratin-inherit",
        @class
      ]}
      {@rest}
    >
      <%= render_slot(@inner_block) %>
    </button>
    """
  end

1 Like