Skip to content

Commit 41d9cae

Browse files
Reject candidates with multiple modifiers (#20466)
<!-- 👋 Hey, thanks for your interest in contributing to Tailwind! **Please ask first before starting work on any significant new features.** It's never a fun experience to have your pull request declined after investing a lot of time and effort into a new feature. To avoid this from happening, we request that contributors create a discussion to first discuss any significant new features. For more info, check out the contributing guide: https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md --> ## Summary <!-- Provide a summary of the issue and the changes you're making. How does your change solve the problem? --> `segment()` preserves empty top-level segments, but the candidate and variant parsers currently use the truthiness of the third segment to detect additional modifiers. As a result, inputs such as `bg-red-500/50/`, `bg-red-500/50//foo`, `group-hover/foo/:flex`, and `group-hover/foo//bar:flex` can be parsed as valid candidates even though they contain multiple slash modifier segments. This change checks the number of segments instead of the value of the third segment. Single modifiers such as `bg-red-500/50` and `group-hover/foo:flex` continue to parse normally, while all additional top-level `/` segments are rejected. ## Test plan <!-- Explain how you tested your changes. Include the exact commands that you used to verify the change works and include screenshots/screen recordings of the update behavior in the browser if applicable. --> - `pnpm exec vitest run packages/tailwindcss/src/candidate.test.ts --hideSkippedTests` - `pnpm exec vitest run packages/tailwindcss/src --hideSkippedTests` - `pnpm exec prettier --check packages/tailwindcss/src/candidate.ts packages/tailwindcss/src/candidate.test.ts` `pnpm --filter=tailwindcss lint` was also attempted, but currently fails on existing cross-package dependency and Bun type errors unrelated to this change. --------- Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
1 parent f723e83 commit 41d9cae

3 files changed

Lines changed: 32 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3232
- Don't generate utilities when a modifier is used that would otherwise be silently ignored (e.g. `rounded-sm/[5]`, `shadow-sm/foo`, `stroke-2/50`) ([#20419](https://github.com/tailwindlabs/tailwindcss/pull/20419))
3333
- Only normalize top-level `and`, `or`, and `not` keywords in `supports-[…]` variants (e.g. `selector(a: not (.foo))``selector(a:not(.foo))`) ([#20420](https://github.com/tailwindlabs/tailwindcss/pull/20420))
3434
- Don't warn about Angular's `::ng-deep` and `:host-context()` when optimizing CSS ([#20434](https://github.com/tailwindlabs/tailwindcss/pull/20434))
35+
- Don't generate CSS for candidates containing an empty additional modifier (e.g. `bg-red-500/50/` and `group-hover/foo//bar:flex`) ([#20466](https://github.com/tailwindlabs/tailwindcss/pull/20466))
3536

3637
## [4.3.3] - 2026-07-16
3738

packages/tailwindcss/src/candidate.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,8 @@ it('should not parse functional utilities with multiple modifiers', () => {
516516
utilities.functional('bg', () => [])
517517

518518
expect(run('bg-red-1/2/3', { utilities })).toMatchInlineSnapshot(`[]`)
519+
expect(run('bg-red-500/50/', { utilities })).toMatchInlineSnapshot(`[]`)
520+
expect(run('bg-red-500/50//foo', { utilities })).toMatchInlineSnapshot(`[]`)
519521
})
520522

521523
it('should parse a utility with an arbitrary value', () => {
@@ -1416,6 +1418,19 @@ it('should parse a functional variant with a modifier', () => {
14161418
`)
14171419
})
14181420

1421+
it('should not parse variants with multiple modifiers', () => {
1422+
let utilities = new Utilities()
1423+
utilities.static('flex', () => [])
1424+
1425+
let variants = new Variants()
1426+
variants.static('hover', () => {})
1427+
variants.compound('group', Compounds.StyleRules, () => {})
1428+
1429+
expect(run('group-hover/foo:flex', { utilities, variants })).toHaveLength(1)
1430+
expect(run('group-hover/foo/:flex', { utilities, variants })).toEqual([])
1431+
expect(run('group-hover/foo//bar:flex', { utilities, variants })).toEqual([])
1432+
})
1433+
14191434
it('should parse a functional variant starting with @', () => {
14201435
let utilities = new Utilities()
14211436
utilities.static('flex', () => [])

packages/tailwindcss/src/candidate.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,14 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter
372372
}
373373
}
374374

375+
// If there's more than one modifier, the utility is invalid.
376+
//
377+
// E.g.:
378+
//
379+
// - `bg-red-500/50/50`
380+
let parts = segment(base, '/')
381+
if (parts.length > 2) return
382+
375383
// Figure out the new base and the modifier segment if present.
376384
//
377385
// E.g.:
@@ -381,14 +389,7 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter
381389
// ^^^^^^^^^^ -> Base without modifier
382390
// ^^ -> Modifier segment
383391
// ```
384-
let [baseWithoutModifier, modifierSegment = null, additionalModifier] = segment(base, '/')
385-
386-
// If there's more than one modifier, the utility is invalid.
387-
//
388-
// E.g.:
389-
//
390-
// - `bg-red-500/50/50`
391-
if (additionalModifier) return
392+
let [baseWithoutModifier, modifierSegment = null] = parts
392393

393394
let parsedModifier = modifierSegment === null ? null : parseModifier(modifierSegment)
394395

@@ -707,17 +708,18 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia
707708

708709
// Static, functional and compound variants
709710
{
710-
// group-hover/group-name
711-
// ^^^^^^^^^^^ -> Variant without modifier
712-
// ^^^^^^^^^^ -> Modifier
713-
let [variantWithoutModifier, modifier = null, additionalModifier] = segment(variant, '/')
714-
715711
// If there's more than one modifier, the variant is invalid.
716712
//
717713
// E.g.:
718714
//
719715
// - `group-hover/foo/bar`
720-
if (additionalModifier) return null
716+
let parts = segment(variant, '/')
717+
if (parts.length > 2) return null
718+
719+
// group-hover/group-name
720+
// ^^^^^^^^^^^ -> Variant without modifier
721+
// ^^^^^^^^^^ -> Modifier
722+
let [variantWithoutModifier, modifier = null] = parts
721723

722724
let roots = findRoots(variantWithoutModifier, (root) => {
723725
return designSystem.variants.has(root)

0 commit comments

Comments
 (0)