Allow trailing dash in functional utility names (#19696)
## 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>
This commit is contained in:
parent
7a54d15f72
commit
d15d92ca60
3 changed files with 47 additions and 12 deletions
|
|
@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
- _Experimental_: Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Allow trailing dash in functional utility names for backwards compatibility ([#19696](https://github.com/tailwindlabs/tailwindcss/pull/19696))
|
||||
|
||||
## [4.2.0] - 2026-02-18
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -28428,7 +28428,7 @@ describe('custom utilities', () => {
|
|||
test.each([
|
||||
['foo', false], // Simple name, missing '-*' suffix
|
||||
['foo-*', true], // Simple name
|
||||
['foo--*', false], // Root should not end in `-`
|
||||
['foo--*', true], // Root ending in `-` is valid (e.g. `border--*`)
|
||||
['-foo-*', true], // Simple name (negative)
|
||||
['foo-bar-*', true], // With dashes
|
||||
['foo_bar-*', true], // With underscores
|
||||
|
|
@ -28900,6 +28900,38 @@ describe('custom utilities', () => {
|
|||
expect(await compileCss(input, ['tab-3', 'tab-gitlab'])).toEqual('')
|
||||
})
|
||||
|
||||
test('functional utility with double-dash separator', async () => {
|
||||
let input = css`
|
||||
@theme reference {
|
||||
--color-border-0: #e5e7eb;
|
||||
--color-border-1: #d1d5db;
|
||||
--color-border-2: #9ca3af;
|
||||
}
|
||||
|
||||
@utility border--* {
|
||||
border-color: --value(--color-border-*, [color]);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(await compileCss(input, ['border--0', 'border--1', 'border--2']))
|
||||
.toMatchInlineSnapshot(`
|
||||
".border--0 {
|
||||
border-color: var(--color-border-0, #e5e7eb);
|
||||
}
|
||||
|
||||
.border--1 {
|
||||
border-color: var(--color-border-1, #d1d5db);
|
||||
}
|
||||
|
||||
.border--2 {
|
||||
border-color: var(--color-border-2, #9ca3af);
|
||||
}"
|
||||
`)
|
||||
expect(await compileCss(input, ['border--3'])).toEqual('')
|
||||
})
|
||||
|
||||
test('resolving values from `@theme`, with `--tab-size-*` syntax', async () => {
|
||||
let input =
|
||||
// Explicitly not using the css tagged template literal so that
|
||||
|
|
|
|||
|
|
@ -6659,22 +6659,21 @@ export function isValidFunctionalUtilityName(name: string): boolean {
|
|||
let root = match[0]
|
||||
let value = name.slice(root.length)
|
||||
|
||||
// Root should not end in `-` if there is no value
|
||||
//
|
||||
// `tab-size--*`
|
||||
// --------- Root
|
||||
// -- Suffix
|
||||
//
|
||||
// Because with default values, this could match `tab-size-` which is invalid.
|
||||
if (value.length === 0 && root.endsWith('-')) {
|
||||
return false
|
||||
}
|
||||
|
||||
// No remaining value is valid
|
||||
//
|
||||
// `tab-size-*`
|
||||
// -------- Root
|
||||
// -- Suffix
|
||||
//
|
||||
// Backwards compatibility: a root ending in `-` was valid and correctly
|
||||
// scanned by Oxide. This means that custom utilities can result in candidates
|
||||
// such as `foo--bar`.
|
||||
//
|
||||
// We might want to revisit this for Tailwind CSS v5, but for now we have to
|
||||
// make it backwards compatible.
|
||||
//
|
||||
// PR: https://github.com/tailwindlabs/tailwindcss/pull/19696
|
||||
//
|
||||
if (value.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue