PhiaUi.ClassMerger (phia_ui v0.1.16)

Copy Markdown View Source

Native Tailwind CSS class merger for PhiaUi.

Provides cn/1 — the primary utility for composing and merging Tailwind class strings. It is the Elixir equivalent of the JavaScript combination of clsx (conditional class joining) and tailwind-merge (conflict resolution).

Designed to be imported in component modules:

import PhiaUi.ClassMerger, only: [cn: 1]

Behaviour

  • Accepts a list of class strings, nil, or false values.
  • Falsy values (nil, false) are silently discarded, making it safe to pass conditional expressions directly in the list.
  • Individual strings may contain multiple space-separated tokens.
  • Exact duplicate tokens are deduplicated; the last occurrence wins, matching the standard CSS cascade convention.
  • When two classes belong to the same Tailwind conflict group (i.e. they both target the same CSS property — see PhiaUi.ClassMerger.Groups), the last one wins and the earlier one is removed entirely.
  • Results are memoised in PhiaUi.ClassMerger.Cache (an ETS table) for zero-cost repeated calls with the same inputs.

Conflict Resolution Examples

Padding axis conflict — px-2 and px-4 both set padding-inline; the later px-4 wins:

iex> PhiaUi.ClassMerger.cn(["px-2 py-1", "px-4"])
"py-1 px-4"

Background colour conflict — only one bg-* class is kept:

iex> PhiaUi.ClassMerger.cn(["bg-blue-500", "bg-red-600"])
"bg-red-600"

Text size conflict — text-sm and text-lg both set font-size:

iex> PhiaUi.ClassMerger.cn(["text-sm font-bold", "text-lg"])
"font-bold text-lg"

Text colour vs text size — these are different groups and are both kept:

iex> PhiaUi.ClassMerger.cn(["text-sm", "text-red-500"])
"text-sm text-red-500"

Falsy values are silently ignored:

iex> PhiaUi.ClassMerger.cn(["px-4 py-2", nil, false, "font-semibold"])
"px-4 py-2 font-semibold"

Exact duplicates are deduplicated; last occurrence wins:

iex> PhiaUi.ClassMerger.cn(["px-4 py-2", "font-semibold", nil, "px-4"])
"py-2 font-semibold px-4"

Empty list returns an empty string:

iex> PhiaUi.ClassMerger.cn([])
""

Usage in Components

Every PhiaUI component uses cn/1 to merge its base classes with the caller-supplied class override attribute:

def button(assigns) do
  ~H"""
  <button class={cn(["px-4 py-2 rounded", @variant_class, @class])}>
    <%= render_slot(@inner_block) %>
  </button>
  """
end

This lets callers override individual utilities without duplicating the component's full class list:

# Replaces px-4 with px-8; everything else is unchanged.
<.button class="px-8">Wide Button</.button>

Performance

cn/1 is called on every component render. The first call for a given list of inputs runs the full resolution pipeline (tokenise → dedup → join) and writes the result to the ETS cache. Subsequent calls with identical inputs return the cached string directly from ETS in O(1) time, bypassing the pipeline entirely.

Because the ETS table is configured with read_concurrency: true, reads from concurrent LiveView processes are lock-free and do not serialise through any single GenServer process.

Summary

Functions

Merges a list of class values into a single, conflict-resolved class string.

Functions

cn(classes)

@spec cn([String.t() | nil | false]) :: String.t()

Merges a list of class values into a single, conflict-resolved class string.

Accepts String.t(), nil, or false elements. Returns "" for an empty or fully-falsy list. When multiple classes target the same Tailwind utility group, the last one in the list wins and all earlier ones are removed.

Results are memoised in ETS. The first call for a given input list runs the full resolution pipeline; subsequent identical calls are O(1) cache hits.

Examples

iex> PhiaUi.ClassMerger.cn(["px-4 py-2", "font-semibold", nil, "px-4"])
"py-2 font-semibold px-4"

iex> PhiaUi.ClassMerger.cn(["bg-primary", "bg-secondary"])
"bg-secondary"

iex> PhiaUi.ClassMerger.cn([nil, false, nil])
""