tailwind2-6 Exercise 1: Which Class Wins, and What Does twMerge Know?
====================================================================
Setup: a folder with npm install tailwindcss @tailwindcss/cli clsx tailwind-merge plus the files below. Build with
npx @tailwindcss/cli -i s.css -o out.css
Open page.html in Chrome. The numbers below came from a scripted headless Chrome (cdp.mjs, my own DevTools driver) that read the computed style of each element; you can read the same values in DevTools.
--- s.css ---
@import "tailwindcss";
@theme { --text-huge: 4rem; --color-brand: oklch(55% .2 260); }
--- page.html ---
a
b
c
d
Order of the rules in out.css: .bg-blue-500, .bg-red-500, .px-2, .px-8
Result in Chrome:
a class="bg-red-500 bg-blue-500" background oklch(0.637 0.237 25.331) padding-left 0px
b class="bg-blue-500 bg-red-500" background oklch(0.637 0.237 25.331) padding-left 0px
c class="px-8 px-2" padding-left 32px
d class="px-2 px-8" padding-left 32px
--- m.mjs (the merge table) ---
import { clsx } from 'clsx';
import { twMerge, extendTailwindMerge } from 'tailwind-merge';
const show=(l,v)=>console.log(l.padEnd(46),JSON.stringify(v));
show('clsx("px-2", false, "px-8")', clsx("px-2", false && "x", "px-8"));
show('twMerge("px-2 px-8")', twMerge("px-2 px-8"));
show('twMerge("p-4 px-2")', twMerge("p-4 px-2"));
show('twMerge("px-2 p-4")', twMerge("px-2 p-4"));
show('twMerge("bg-red-500 bg-blue-500")', twMerge("bg-red-500 bg-blue-500"));
show('twMerge("text-lg text-brand") (colour+size)', twMerge("text-lg text-brand"));
show('twMerge("text-huge text-lg") custom size', twMerge("text-huge text-lg"));
show('twMerge("text-brand text-red-500") custom colour', twMerge("text-brand text-red-500"));
show('twMerge("hover:bg-red-500 bg-blue-500")', twMerge("hover:bg-red-500 bg-blue-500"));
show('twMerge("rounded-md rounded-lg")', twMerge("rounded-md rounded-lg"));
show('twMerge("w-4 size-8")', twMerge("w-4 size-8"));
show('twMerge("!p-2 p-4") bang', twMerge("!p-2 p-4"));
show('twMerge("p-2! p-4") v4 important', twMerge("p-2! p-4"));
const tm = extendTailwindMerge({ extend: { classGroups: { 'font-size': [{ text: ['huge'] }] } } });
show('extended: text-huge text-lg', tm("text-huge text-lg"));
show('extended: text-lg text-huge', tm("text-lg text-huge"));
Output:
clsx("px-2", false, "px-8") "px-2 px-8"
twMerge("px-2 px-8") "px-8"
twMerge("p-4 px-2") "p-4 px-2"
twMerge("px-2 p-4") "p-4"
twMerge("bg-red-500 bg-blue-500") "bg-blue-500"
twMerge("text-lg text-brand") (colour+size) "text-lg text-brand"
twMerge("text-huge text-lg") custom size "text-huge text-lg"
twMerge("text-brand text-red-500") custom colour "text-red-500"
twMerge("hover:bg-red-500 bg-blue-500") "hover:bg-red-500 bg-blue-500"
twMerge("rounded-md rounded-lg") "rounded-lg"
twMerge("w-4 size-8") "size-8"
twMerge("!p-2 p-4") bang "!p-2 p-4"
twMerge("p-2! p-4") v4 important "p-2! p-4"
extended: text-huge text-lg "text-lg"
extended: text-lg text-huge "text-huge"
WHY THIS WORKS AS AN ANSWER
---------------------------
Elements a and b have the same classes in opposite order and the same colour, and c and d the same padding, so the attribute order has no effect: both classes have equal specificity and the rule that Tailwind
wrote later in out.css wins (red after blue, px-8 after px-2). A component cannot rely on "the caller's class comes last", so conflicting classes must be removed from the string. twMerge does that for
classes it knows: later px-8 replaces px-2; p-4 replaces an earlier px-2 (it covers it) but px-2 after p-4 is kept because it is more specific; a hover: class does not conflict with a plain one because they are
different variants. The custom size is the limit: twMerge has no access to your @theme, so "text-huge text-lg" kept both font sizes. extendTailwindMerge with a font-size group containing "huge" fixed it in both
orders. The custom colour case "text-brand text-red-500" came out right only because an unknown name after text- is treated as a colour; do not rely on that for sizes, shadows or radii. The two "!important" lines kept both classes; I did not investigate that beyond noting it.