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 for class, or nil if unknown.
Functions
Returns the conflict group for class, or nil if unknown.
Returns a {prefix, group} tuple where prefix is the variant prefix
string (e.g. "sm:", "hover:sm:") or nil for unprefixed classes.
Two classes with the same {prefix, group} tuple conflict; only the last
one is kept by cn/1.
This makes conflict resolution responsive-prefix aware: sm:flex-col and
sm:flex-row conflict (both {"sm:", :flex_direction}), but sm:flex-col
and md:flex-row do not conflict (different prefixes).
Returns nil when the class has no known conflict group — it will be
deduplicated by exact string match instead.
Examples
Background colour group:
iex> PhiaUi.ClassMerger.Groups.group_for("bg-primary")
{nil, :bg}
iex> PhiaUi.ClassMerger.Groups.group_for("bg-red-500")
{nil, :bg}Responsive prefix awareness:
iex> PhiaUi.ClassMerger.Groups.group_for("sm:bg-primary")
{"sm:", :bg}
iex> PhiaUi.ClassMerger.Groups.group_for("md:bg-red-500")
{"md:", :bg}Text size (font-size) group:
iex> PhiaUi.ClassMerger.Groups.group_for("text-sm")
{nil, :text_size}
iex> PhiaUi.ClassMerger.Groups.group_for("text-2xl")
{nil, :text_size}Text colour — different group from text size:
iex> PhiaUi.ClassMerger.Groups.group_for("text-red-500")
{nil, :text_color}
iex> PhiaUi.ClassMerger.Groups.group_for("text-primary")
{nil, :text_color}Text alignment — exact-matched before the text-* catch-all:
iex> PhiaUi.ClassMerger.Groups.group_for("text-center")
{nil, :text_align}Padding — axis-specific group:
iex> PhiaUi.ClassMerger.Groups.group_for("px-4")
{nil, :px}
iex> PhiaUi.ClassMerger.Groups.group_for("p-4")
{nil, :p}Display — exact match:
iex> PhiaUi.ClassMerger.Groups.group_for("flex")
{nil, :display}
iex> PhiaUi.ClassMerger.Groups.group_for("hidden")
{nil, :display}Unknown / custom class returns nil:
iex> PhiaUi.ClassMerger.Groups.group_for("phia-custom")
nil
iex> PhiaUi.ClassMerger.Groups.group_for("my-custom-class")
nil