Skip to content

Commit d15d92c

Browse files
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>
1 parent 7a54d15 commit d15d92c

File tree

3 files changed

+47
-12
lines changed

3 files changed

+47
-12
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- _Experimental_: Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901))
1313

14+
### Fixed
15+
16+
- Allow trailing dash in functional utility names for backwards compatibility ([#19696](https://github.com/tailwindlabs/tailwindcss/pull/19696))
17+
1418
## [4.2.0] - 2026-02-18
1519

1620
### Added

packages/tailwindcss/src/utilities.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28428,7 +28428,7 @@ describe('custom utilities', () => {
2842828428
test.each([
2842928429
['foo', false], // Simple name, missing '-*' suffix
2843028430
['foo-*', true], // Simple name
28431-
['foo--*', false], // Root should not end in `-`
28431+
['foo--*', true], // Root ending in `-` is valid (e.g. `border--*`)
2843228432
['-foo-*', true], // Simple name (negative)
2843328433
['foo-bar-*', true], // With dashes
2843428434
['foo_bar-*', true], // With underscores
@@ -28900,6 +28900,38 @@ describe('custom utilities', () => {
2890028900
expect(await compileCss(input, ['tab-3', 'tab-gitlab'])).toEqual('')
2890128901
})
2890228902

28903+
test('functional utility with double-dash separator', async () => {
28904+
let input = css`
28905+
@theme reference {
28906+
--color-border-0: #e5e7eb;
28907+
--color-border-1: #d1d5db;
28908+
--color-border-2: #9ca3af;
28909+
}
28910+
28911+
@utility border--* {
28912+
border-color: --value(--color-border-*, [color]);
28913+
}
28914+
28915+
@tailwind utilities;
28916+
`
28917+
28918+
expect(await compileCss(input, ['border--0', 'border--1', 'border--2']))
28919+
.toMatchInlineSnapshot(`
28920+
".border--0 {
28921+
border-color: var(--color-border-0, #e5e7eb);
28922+
}
28923+
28924+
.border--1 {
28925+
border-color: var(--color-border-1, #d1d5db);
28926+
}
28927+
28928+
.border--2 {
28929+
border-color: var(--color-border-2, #9ca3af);
28930+
}"
28931+
`)
28932+
expect(await compileCss(input, ['border--3'])).toEqual('')
28933+
})
28934+
2890328935
test('resolving values from `@theme`, with `--tab-size-*` syntax', async () => {
2890428936
let input =
2890528937
// Explicitly not using the css tagged template literal so that

packages/tailwindcss/src/utilities.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6659,22 +6659,21 @@ export function isValidFunctionalUtilityName(name: string): boolean {
66596659
let root = match[0]
66606660
let value = name.slice(root.length)
66616661

6662-
// Root should not end in `-` if there is no value
6663-
//
6664-
// `tab-size--*`
6665-
// --------- Root
6666-
// -- Suffix
6667-
//
6668-
// Because with default values, this could match `tab-size-` which is invalid.
6669-
if (value.length === 0 && root.endsWith('-')) {
6670-
return false
6671-
}
6672-
66736662
// No remaining value is valid
66746663
//
66756664
// `tab-size-*`
66766665
// -------- Root
66776666
// -- Suffix
6667+
//
6668+
// Backwards compatibility: a root ending in `-` was valid and correctly
6669+
// scanned by Oxide. This means that custom utilities can result in candidates
6670+
// such as `foo--bar`.
6671+
//
6672+
// We might want to revisit this for Tailwind CSS v5, but for now we have to
6673+
// make it backwards compatible.
6674+
//
6675+
// PR: https://github.com/tailwindlabs/tailwindcss/pull/19696
6676+
//
66786677
if (value.length === 0) {
66796678
return true
66806679
}

0 commit comments

Comments
 (0)