Skip to content

Improve @utility name validation#19524

Merged
RobinMalfait merged 5 commits intomainfrom
fix/issue-19505
Jan 6, 2026
Merged

Improve @utility name validation#19524
RobinMalfait merged 5 commits intomainfrom
fix/issue-19505

Conversation

@RobinMalfait
Copy link
Member

@RobinMalfait RobinMalfait commented Jan 5, 2026

This PR improves the validation of allowed @utility … names.

Each @utility name should be a valid Tailwind CSS class, so new syntaxes should not be allowed, e.g. foo/bar/baz would be invalid.

We already enforce this behavior but not consistently. The Oxide scanner that scans all your source files for potential Tailwind CSS classes does enforce all of these rules already. So if you used @utility foo/bar/baz {}, the Oxide scanner would not pick up foo/bar/baz as a valid class name, so for that reason it's not a breaking change.

Where we didn't enforce it is in places where you use the development-only CDN or Tailwind Play. That's because those environments don't use the Oxide at all, and get the classes from the DOM directly and pass it to Tailwind's compiler.

This PR moves some of these validation rules into Tailwind's core when defining custom @utility utilities.

Fixes: #19505

Test plan

  1. Existing tests still pass
  2. Added a regression test for the linked issue
  3. Added new tests with valid / invalid @utility names

I also confirmed with Oxide to know which classes were actually valid and which ones are invalid.

Given this input CSS:

@utility foo { color: red }
@utility foo_ { color: red }     /* This one looks invalid to me, but it works today */
                                 /* and I don't want to introduce unnecessary breaking changes. */
@utility foo-1.5 { color: red }
@utility foo-123 { color: red }
@utility -foo { color: red }
@utility foo-bar { color: red }
@utility foo_bar { color: red }
@utility foo-50% { color: red }
@utility foo-1/2 { color: red }

And this HTML:

<!-- Extracted: -->
<div class="foo foo_ foo-123 -foo foo-bar foo_bar foo-50% foo-1/2 foo-1.5"></div>

<!-- Not Extracted: -->
<div class="Foo -Foo foo-1/ foo- foo-p% foo-1..5 foo.bar foo..bar "></div>

Then all classes in the Extracted section are found. One funny thing in the not extracted section is that the bar in foo.bar and foo..bar is also extracted. Feels like a potential bug, but out of scope for this PR.

image

@RobinMalfait RobinMalfait marked this pull request as ready for review January 5, 2026 19:03
@RobinMalfait RobinMalfait requested a review from a team as a code owner January 5, 2026 19:03
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 5, 2026

Walkthrough

This change aligns Tailwind's @utility name validation with Oxide scanner rules. It adds and exports two validator functions, isValidStaticUtilityName and isValidFunctionalUtilityName, plus a UTILITY_ROOT pattern, and replaces prior inline regex checks with these validators in createCssUtility. Tests were added to verify valid and invalid utility name patterns, including enforcement around forward slash usage.

Pre-merge checks

✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Improve @utility name validation' directly and accurately summarizes the main change in the PR, which refactors utility name validation logic.
Description check ✅ Passed The PR description clearly explains the validation improvements, documents the context with Oxide scanner behavior, provides concrete examples of valid/invalid names, and references the linked issue.
Linked Issues check ✅ Passed The PR successfully addresses the linked issue #19505 by implementing consistent validation across environments. New utility name validation functions reject multi-slash names (e.g., ui/button/sm) matching Oxide scanner rules, and regression tests verify this behavior.
Out of Scope Changes check ✅ Passed All changes are directly scoped to utility name validation: new validator functions, updated validation logic in createCssUtility, and comprehensive test coverage. No unrelated modifications detected.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/tailwindcss/src/utilities.test.ts (1)

27211-27234: Consider adding explicit test cases for multiple slashes (core issue #19505).

The test suite covers many edge cases well, but given that issue #19505 specifically concerns multiple slashes in utility names (e.g., ui/button/sm), consider adding explicit test cases:

🔎 Suggested additions for slash handling clarity
     ['foo-4/snug', true], // Bare value with named modifier
     ['foo_', true], // This is supported today, so let's not break it
+    ['foo/bar', true], // Single slash as modifier separator
 
     ['Foo', false], // Starting with uppercase letter is invalid
     ['-Foo', false], // Starting with uppercase letter is invalid (negative)
     ['foo-', false], // Should not end with a dash
     ['foo-1/', false], // Invalid fraction/modifier
+    ['foo/bar/baz', false], // Multiple slashes are invalid (issue #19505)
     ['foo-p%', false], // Invalid percentage

This would serve as a clear regression test for the specific issue being fixed and document the expected single vs. multiple slash behavior directly in the validation function's test suite.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c07d4a7 and 908111c.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • packages/tailwindcss/src/index.test.ts
  • packages/tailwindcss/src/utilities.test.ts
  • packages/tailwindcss/src/utilities.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/tailwindcss/src/index.test.ts (2)
integrations/utils.ts (2)
  • test (81-456)
  • css (519-519)
packages/tailwindcss/src/test-utils/run.ts (1)
  • compileCss (4-11)
🔇 Additional comments (6)
CHANGELOG.md (1)

16-16: Changelog entry for @utility validation looks accurate and consistent

The wording clearly describes the behavior change, matches the surrounding style in the “Fixed” section, and correctly links to the PR. No further adjustments needed.

packages/tailwindcss/src/index.test.ts (1)

4663-4697: LGTM! Comprehensive test for utility name validation.

This test effectively validates the new @utility name constraints introduced to fix issue #19505:

  • Positive case: Correctly verifies that a single / (modifier separator) in ui/button is valid and generates properly escaped CSS
  • Negative case: Correctly verifies that multiple / characters in ui/button/sm trigger a validation error

The test structure is excellent with clear inline snapshots documenting the expected behavior for both valid and invalid inputs. This ensures the Oxide scanner rules are consistently enforced at the core level.

packages/tailwindcss/src/utilities.ts (2)

6429-6568: LGTM! Excellent implementation of utility name validation.

The validation logic correctly enforces all the rules described in the PR objectives:

✅ Valid root pattern: optional -, lowercase letter start, then [a-zA-Z0-9_-]*
✅ Single slash only as modifier separator, must be followed by characters
✅ Dots only between digits, prevents consecutive dots
✅ Percent only at end when preceded by digit
✅ Character-by-character validation for static utilities
✅ Functional utilities correctly reject characters between root and -* suffix

The use of character codes for performance is well-balanced with readability through clear constant names and comprehensive comments. Edge cases like empty strings, trailing dashes, and invalid character positions are all properly handled.


5835-5835: LGTM! Validation functions correctly integrated.

The new validation functions are properly used at the call sites where @utility names need to be validated:

  • Line 5835: Functional utilities (* suffix)
  • Line 6184: Static utilities

This successfully moves validation from the Oxide scanner into Tailwind core as intended.

Also applies to: 6184-6184

packages/tailwindcss/src/utilities.test.ts (2)

4-4: LGTM!

The import correctly brings in the two new validation functions that are being tested.


27236-27249: LGTM!

The functional utility name tests comprehensively cover the validation rules: the -* suffix requirement, negative prefixes, uppercase restrictions, and special character rejection.

- We check for a valid root which should optionally start with `-`, and
  be followed by `[a-z]`. After that, only `[a-zA-Z0-9_-]` is valid for
  the root.
- For static utilities, the remaining part (the value) can include:
  - `.`: but not consecutive ones, e.g. `foo..bar` is invalid
  - `%`: but only at the end and preceded by a digit, e.g. `foo-x%` and
    `foo-%-bar` are invalid
  - `/`: as the modifier, but there can only be one, and must be
    followed by another character, e.g.: `foo/bar/baz` and `foo/` are
    invalid
- For functional utilities, we need a valid root and a valid `-*`
  suffix. The remaining "value" part should be empty.
These are the ones that represent the linked issue even though we have a
dedicated regrression test for it.
@RobinMalfait RobinMalfait enabled auto-merge (squash) January 6, 2026 11:51
@RobinMalfait RobinMalfait disabled auto-merge January 6, 2026 11:51
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/tailwindcss/src/utilities.test.ts (1)

27211-27251: Good test coverage for the new validation functions.

The test cases comprehensively cover the validation rules for both static and functional utility names, including the key case foo/bar/baz that addresses issue #19505. The use of test.each keeps the tests readable and maintainable.

Minor observation: The test descriptions ('valid static utility name "%s" (%s)') say "valid" but also test invalid cases. Consider rewording to something like 'isValidStaticUtilityName("%s") should be %s' for clarity, though this is purely stylistic.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 908111c and 2a9a4f7.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • packages/tailwindcss/src/index.test.ts
  • packages/tailwindcss/src/utilities.test.ts
  • packages/tailwindcss/src/utilities.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/tailwindcss/src/index.test.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/tailwindcss/src/utilities.test.ts (1)
packages/tailwindcss/src/utilities.ts (2)
  • isValidStaticUtilityName (6443-6530)
  • isValidFunctionalUtilityName (6532-6568)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Linux
  • GitHub Check: Linux / upgrade
  • GitHub Check: Linux / postcss
  • GitHub Check: Linux / cli
  • GitHub Check: Linux / vite
🔇 Additional comments (6)
CHANGELOG.md (1)

17-17: Changelog entry is consistent and clear

The new bullet follows the existing style (imperative verb, scoped to @utility, includes PR link) and accurately describes the change in validation behavior.

packages/tailwindcss/src/utilities.test.ts (1)

4-4: LGTM!

Import correctly added for the validation functions under test.

packages/tailwindcss/src/utilities.ts (4)

6429-6442: LGTM! Clean constant definitions for validation logic.

The UTILITY_ROOT pattern correctly enforces that utility names must start with a lowercase letter (after an optional leading dash), which aligns with the Oxide scanner rules and properly rejects invalid names like Foo or -Foo mentioned in the PR objectives.


6443-6530: LGTM! Comprehensive validation logic correctly implements Oxide scanner rules.

The function properly handles all the edge cases mentioned in the PR objectives:

  • ✅ Rejects multiple slashes: foo/bar/baz
  • ✅ Rejects trailing slash: foo-1/
  • ✅ Rejects trailing dash: foo-
  • ✅ Rejects dots not between digits: foo.bar, foo-1..5
  • ✅ Allows valid patterns: foo-1.5, foo-50%, foo-1/2

The character-by-character validation with proper state tracking (e.g., seenSlash) is efficient and correct.


6532-6568: LGTM! Functional utility name validation is correct.

The function properly validates functional utilities (ending with -*) and correctly rejects invalid patterns:

  • ✅ Accepts simple functional utilities: tab-size-*
  • ✅ Rejects double-dash before suffix: tab-size--*
  • ✅ Rejects additional characters before suffix: tab-size-[…]-*

The validation ensures functional utilities maintain a clean <root>-* pattern only.


5835-5835: LGTM! Validation functions properly integrated into utility creation.

The new validation functions are correctly invoked in createCssUtility to ensure all custom @utility definitions are validated consistently across environments (Oxide scanner, CLI/Vite, CDN, Tailwind Play), which addresses the core objective of fixing issue #19505.

Also applies to: 6184-6184

@RobinMalfait RobinMalfait merged commit f82ac39 into main Jan 6, 2026
7 checks passed
@RobinMalfait RobinMalfait deleted the fix/issue-19505 branch January 6, 2026 11:54
RobinMalfait added a commit that referenced this pull request Feb 19, 2026
## Problem

Tailwind 4.2.0 introduced stricter `@utility` name validation (#19524)
that rejects functional utility names where the root ends with a dash
after stripping the `-*` suffix. This breaks a valid and useful naming
pattern where a double dash separates the CSS property from a value
scale:

```css
@Utility border--* {
  border-color: --value(--color-border-*, [color]);
}
```

This produces: `border--0`, `border--1`, `border--2`, etc.

The error message is:

> `@utility border--*` defines an invalid utility name. Utilities should
be alphanumeric and start with a lowercase letter.

## Why this pattern matters

The double-dash convention creates a clear visual grammar in class
names. The first segment names the CSS property, and the double dash
separates it from the semantic scale value. In a dense className string
like `border border--0 background--0 content--4`, the scale values (0,
0, 4) are immediately scannable, distinct from the single-dash property
names around them.

This pattern is actively used in production design systems for semantic
color scales (background, content, border, shadow) with values from
0-10.

## Why the restriction is unnecessary

The validation comment states the concern is that `border--*` could
match the bare class `border-` when using default values. However, this
edge case is already handled:

1. **`findRoots` in `candidate.ts`** (line 887) already rejects empty
values: `if (root[1] === '') break`
2. **The Oxide scanner** already extracts double-dash candidates
correctly, as confirmed by existing tests: `("items--center",
vec!["items--center"])`

The candidate parser and scanner both handle this case. The validation
was an overcorrection.

## Changes

- Removed the trailing-dash check from `isValidFunctionalUtilityName` in
`utilities.ts`
- Updated the existing unit test from `['foo--*', false]` to `['foo--*',
true]`
- Added an integration test proving `@utility border--*` compiles
correctly with theme values

## Test results

All 4121 tests pass across the tailwindcss package, including the new
integration test.

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Content Scanner ignores classes with 2 slashes (e.g. ui/button/sm)

1 participant

Comments