Published: August 18, 2026 · by Srinu Desetti · tailwindlabs/tailwindcss#20419 (opens in a new tab) · merged August 14, 2026
Stopping Silently Ignored Invalid Modifiers in Tailwind CSS
Tailwind CSS is the utility-first framework used across the modern web. My contribution — merged into Tailwind's core — fixes a bug where classes containing invalid modifiers still generated CSS, because Tailwind found the normal theme value and accidentally ignored the invalid modifier.
The correct behavior is simple:
Invalid modifier → reject the whole candidate → generate NO CSSAffected examples from the PR: rounded-sm/[5], shadow-sm/foo, inset-shadow-sm/foo, text-shadow/foo, drop-shadow/foo — and, added during review, stroke-2/foo.
Background: Utilities, Values, and Modifiers
When you write rounded-sm, Tailwind parses it into a utility (rounded) and a value (sm), looks up sm in the theme (--radius-sm), and emits border-radius: var(--radius-sm).
A modifier is extra information after /. For colors it means opacity — bg-red-500/50 is valid: red background at 50% opacity. But not every utility supports modifiers. rounded-sm/[5] is meaningless — rounded controls border radius, and /[5] isn't a supported modifier for it.
Internally, Tailwind parses each class into a candidate:
The Bug
Before my PR, rounded-sm/[5] was silently treated like rounded-sm:
rounded-sm/[5]
↓
find "sm" in theme → FOUND
↓
generate rounded-sm CSS
↓
ignore /[5] ❌The developer wrote an invalid class, Tailwind produced CSS anyway, and nothing indicated the class was wrong.
Why only one path was broken
Tailwind's shared functionalUtility() handles several paths, and most already validated the modifier:
| Path | Example | Modifier check? |
|---|---|---|
| No value | rounded/foo | ✅ rejected |
| Arbitrary value | rounded-[4px]/foo | ✅ rejected |
| Named theme value | rounded-sm/[5] | ❌ missing — the bug |
An interesting wrinkle: rounded-sm/5 (named modifier) appeared to work — but partly by accident. The parser effectively looked up sm/5 in the theme, found nothing, and bailed. The missing validation only became visible with rounded-sm/[5], where the arbitrary modifier is parsed separately and the plain sm lookup succeeds.
The Fix
One guard after successful theme resolution:
Read as a sentence: "I found the normal theme value, but the user also supplied a modifier that is not part of a valid fraction — reject the class."
The three conditions each matter:
value !== null— the theme lookup succeeded (smexists).candidate.modifier— the user provided a modifier ([5]).!candidate.value.fraction— this is not legitimate fraction syntax.
The fraction exception is critical: w-1/2 means width 50%, and the /2 is part of a valid fraction, not a modifier. Without that third condition the guard would break valid classes — fixing one bug by creating another.
The Shadow Family Had the Same Problem
Shadows are subtler because they legitimately support opacity modifiers: shadow-sm/25 is valid (shadow at 25% opacity). The validation question is different: did the modifier produce a valid opacity?
"If the user provided a modifier, but it did not produce a valid opacity value, stop." So shadow-sm/25 → alpha exists → continue; shadow-sm/foo → alpha undefined → no CSS.
Some shadow branches already had this guard; others didn't. The PR made every branch consistent:
Before: After:
shadow → FORGOT shadow → validate
inset-shadow → FORGOT inset-shadow → validate
text-shadow → FORGOT text-shadow → validate
drop-shadow (def) → FORGOT drop-shadow (def) → validate
some drop-shadow → validate some drop-shadow → validateDuring review, the maintainer spotted the same pattern in one more family — stroke-2/foo would also compile — so the PR grew to cover stroke-* with handling and tests. A good example of review expanding a fix to every instance of the underlying pattern.
The Testing Lesson: Passing Tests That Test Nothing
There was already a test asserting drop-shadow/foo generates no CSS. Why didn't it catch the bug?
Because it ran without the relevant theme value. The theme lookup failed first, so the test passed — for the wrong reason:
Bad test: theme lookup fails → no CSS (modifier validation never reached)
Good test: theme lookup succeeds → modifier checked → invalid → no CSSMy PR's tests define the theme values (--shadow, --shadow-sm, …) so execution actually reaches modifier validation. These tests fail without the fix and pass with it — 398 tests passing in utilities.test.ts.
The lesson worth keeping: a passing test does not always test the right thing. If you're testing "invalid input is rejected at step 3", you must ensure execution gets past steps 1 and 2 — otherwise an earlier failure makes the test pass without ever exercising the rejection you wrote it for.
Impact
- Invalid classes no longer silently produce CSS —
rounded-sm/[5]generatingrounded-sm's output hid real mistakes in developers' markup. - Modifier validation is now consistent across the utility paths and the whole shadow/stroke families — the rule "unsupported modifier → no CSS" applies everywhere, not just on the paths that remembered it.
- The test suite got structurally stronger: the regression tests reach the validation code path instead of passing on an earlier accident.
What I Contributed
- Identified that the named-theme-value path skipped modifier validation while sibling paths enforced it.
- Implemented the guard with the fraction exception, preserving valid
w-1/2-style syntax. - Added the same validation to the missing shadow branches, and
stroke-*after maintainer review. - Strengthened the tests so they actually reach modifier validation.
- Collaborated with the Tailwind maintainers until the PR was merged.
Changed files: packages/tailwindcss/src/utilities.ts, packages/tailwindcss/src/utilities.test.ts, CHANGELOG.md.
View the pull request → tailwindlabs/tailwindcss#20419 (opens in a new tab)
← Previous: iovalkey socket timeout fix · All contributions →
Frequently Asked Questions
Why did rounded-sm/[5] generate CSS before this fix?
Tailwind's named-theme-value path looked up sm in the theme, found --radius-sm, and generated CSS — without checking whether the candidate also carried an unsupported modifier. The [5] was simply thrown away. Sibling paths (no value, arbitrary value) already had the check; this one was missing it.
Why does the fix have a fraction exception?
Slash syntax is sometimes valid value syntax, not a modifier: w-1/2 means width 50%. The guard rejects a candidate only when the modifier is not part of a legitimate fraction — otherwise the fix would break valid classes while fixing invalid ones.
Why didn't the existing tests catch this bug?
The old test for drop-shadow/foo ran without the relevant theme value, so the theme lookup failed and no CSS was generated — the test passed without ever reaching modifier validation. The new tests define the theme values so the lookup succeeds and the modifier check is actually exercised.
Is this a breaking change?
Only for markup that was already wrong: classes like rounded-sm/[5] that previously produced CSS silently now correctly produce none. Valid classes — including fraction syntax like w-1/2 and legitimate opacity modifiers like shadow-sm/25 — behave exactly as before.