PineUiPhoenix.Internal.Class (Pine UI v0.2.0)

Copy Markdown View Source

Conflict-aware Tailwind class merging.

Why this exists

The naive approach to letting callers customise a component is string concatenation:

class={"bg-indigo-600 px-4 " <> @class}

This does not work. With Tailwind, the winner between two conflicting utilities is decided by their order in the generated stylesheet, not by their order in the class attribute. class="bg-indigo-600 bg-red-500" renders whichever of the two Tailwind happened to emit later — which the caller cannot predict or control.

cx/1 fixes this by removing the earlier of any two classes that target the same CSS property, so the last one genuinely wins:

iex> cx(["bg-indigo-600 px-4 py-2 rounded-md", "bg-red-500 px-6"])
"py-2 rounded-md bg-red-500 px-6"

Variants are scoped

Classes only conflict when their variant prefixes match, so light and dark values coexist:

iex> cx(["bg-white dark:bg-slate-900", "dark:bg-black"])
"bg-white dark:bg-black"

Variant order does not matter — hover:md:flex and md:hover:block conflict.

Escape hatches

This resolver covers the utility groups Pine UI actually emits. It is deliberately conservative: an unrecognised class is always kept, so a class this module has never heard of can never be silently dropped.

If you already run a full-featured merger, delegate to it:

config :pine_ui_phoenix, class_merge: {TwMerge, :merge, []}

The configured function receives a single space-joined string and must return a string.

Tailwind's ! important modifier is also honoured — !bg-red-500 is tracked separately from bg-red-500, and wins in the cascade regardless of order.

Summary

Functions

Merges Tailwind class inputs, dropping earlier classes that conflict with later ones.

Merges two class inputs. See cx/1.

Merges three class inputs. See cx/1.

Types

input()

@type input() :: String.t() | nil | false | [input()]

Functions

cx(inputs)

@spec cx(input()) :: String.t()

Merges Tailwind class inputs, dropping earlier classes that conflict with later ones.

Accepts strings, nil, false, and arbitrarily nested lists of the same — matching what HEEx already allows in a class={...} attribute. Falsy entries are discarded, which makes conditional classes read naturally:

cx([base, @variant_classes, @disabled && "opacity-50 cursor-not-allowed", @class])

cx(base, override)

@spec cx(input(), input()) :: String.t()

Merges two class inputs. See cx/1.

cx(base, override, extra)

@spec cx(input(), input(), input()) :: String.t()

Merges three class inputs. See cx/1.