Maps Tailwind CSS utility class tokens to their conflict group.
The Conflict Group Concept
Two Tailwind classes "conflict" when they both set the same CSS property.
For example, px-2 and px-4 both set padding-inline-start and
padding-inline-end. Applying both classes to an element results in only
one value being active (whichever appears last in the stylesheet), which
makes the earlier class redundant and confusing.
cn/1 uses this module to detect and resolve such conflicts at merge time:
only the last class in a conflict group is kept in the output.
Each conflict group is identified by an atom (e.g. :px, :bg,
:text_size). Two tokens with the same group atom conflict; two tokens
with different group atoms are independent and both kept.
Lookup Strategy
group_for/1 uses a four-step cascade to classify any token. Steps are
ordered from most specific to least specific, and the first match wins:
exact_group/1— O(1) compile-time map lookup. Handles classes that must be matched precisely to avoid false positives (e.g.flexmatches both the display group and theflex-*prefix rule). Also handlestext-{align}classes that must be separated from thetext-{color}catch-all in step 2.special_group/1— Handles ambiguoustext-*andfont-*prefixes that cover multiple independent CSS properties:text-sm / text-lg→:text_size(font-size)text-red-500 / text-primary→:text_color(color)font-bold / font-semibold→:font_weight(font-weight)font-sans / font-mono→:font_family(font-family)
prefix_group/1— Iterates the@prefix_rulestable (ordered longest-first) and returns the group for the first matching prefix. This covers spacing, sizing, layout, and decoration utilities.standalone_group/1— Catches multi-word classes that share a prefix with an unrelated group (e.g.flex-row/flex-colfor flex direction, which must not match theflex-*prefix in the display group).
Why MapSet for Membership Tests?
The compile-time constants (@text_size_classes, @font_weight_classes,
@font_family_classes, @display_classes, etc.) are stored as MapSet
values rather than plain lists. This gives O(1) average-case membership
tests (class in @text_size_classes) instead of O(n) linear scans, which
matters because group_for/1 is called for every token in every cn/1
call.
MapSets are built at compile time using module attributes, so there is no runtime allocation cost — the set is baked into the BEAM bytecode as a literal.
Why Prefix Rules Are Ordered Longest-First
The prefix-rule table is scanned linearly from top to bottom; the first matching prefix wins. Ordering by descending prefix length prevents shorter prefixes from matching classes that belong to a more specific group.
Example: gap-x-4 must match {"gap-x-", :gap_x} before {"gap-", :gap}.
If "gap-" appeared first, gap-x-4 would be incorrectly assigned to the
general :gap group instead of the axis-specific :gap_x group, causing
gap-x-4 and gap-4 to be treated as conflicting (they are independent CSS
properties: column-gap vs gap).
Similarly, px-4 must match {"px-", :px} before {"p-", :p} — they set
different CSS properties (padding-inline vs all-sides padding).
Returns
Returns nil for classes that don't belong to any known group (arbitrary /
custom classes), in which case exact-match deduplication applies instead.
Summary
Functions
Returns the conflict group atom for class, or nil if unknown.
Functions
Returns the conflict group atom for class, or nil if unknown.
The returned atom identifies the CSS property group that the class belongs
to. Two classes with the same group atom conflict; only the last one is kept
by cn/1. A nil return means the class is not in any known conflict group
and will be deduplicated by exact string match instead.
Examples
Background colour group:
iex> PhiaUi.ClassMerger.Groups.group_for("bg-primary")
:bg
iex> PhiaUi.ClassMerger.Groups.group_for("bg-red-500")
:bgText size (font-size) group:
iex> PhiaUi.ClassMerger.Groups.group_for("text-sm")
:text_size
iex> PhiaUi.ClassMerger.Groups.group_for("text-2xl")
:text_sizeText colour — different group from text size:
iex> PhiaUi.ClassMerger.Groups.group_for("text-red-500")
:text_color
iex> PhiaUi.ClassMerger.Groups.group_for("text-primary")
:text_colorText alignment — exact-matched before the text-* catch-all:
iex> PhiaUi.ClassMerger.Groups.group_for("text-center")
:text_alignPadding — axis-specific group:
iex> PhiaUi.ClassMerger.Groups.group_for("px-4")
:px
iex> PhiaUi.ClassMerger.Groups.group_for("p-4")
:pDisplay — exact match:
iex> PhiaUi.ClassMerger.Groups.group_for("flex")
:display
iex> PhiaUi.ClassMerger.Groups.group_for("hidden")
:displayUnknown / custom class returns nil:
iex> PhiaUi.ClassMerger.Groups.group_for("phia-custom")
nil
iex> PhiaUi.ClassMerger.Groups.group_for("my-custom-class")
nil