Fix infinite loop when using @variant inside @custom-variant that points to another @custom-variant (#19633)

This PR fixes an infinite loop when you use a `@variant` inside of a
`@custom-variant`, where the `@variant` used is another
`@custom-variant`.

The issue stems from the fact that a `@custom-variant` can use a `@slot`
that we have to replace with the proper AST nodes. However in this
setup, the AST nodes will include a `@slot` node as well, which causes
us to replace the `@slot` again, and so on, causing an infinite loop.

```css
@custom-variant a {
  @slot;
}

@custom-variant b {
  @variant a {
    @slot;
  }
}
```

The solution here is to replace the `@slot` nodes and then skip walking
the nodes that were just inserted. This does mean that we end up with a
`@slot` node in the final AST but that's not a real issue because that
will get replaced later when handling the next `@custom-variant`.

## Test plan

1. Existing tests still pass
2. Added a regression test to ensure that the infinite loop does not
happen anymore
3. Added additional tests to ensure that the behavior is correct

Thanks @wongjn for your initial debugging help and providing a test case
as well!

Fixes: #19618
This commit is contained in:
Robin Malfait 2026-02-02 14:42:31 +01:00 • committed by GitHub
parent d52c94ff5f
commit df96ea5eba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 93 additions and 1 deletions

View file

@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Detect utilities when containing capital letters followed by numbers ([#19465](https://github.com/tailwindlabs/tailwindcss/pull/19465))
- Fix class extraction for Rails' strict locals ([#19525](https://github.com/tailwindlabs/tailwindcss/pull/19525))
- Align `@utility` name validation with Oxide scanner rules ([#19524](https://github.com/tailwindlabs/tailwindcss/pull/19524))
- Fix infinite loop when using `@variant` inside `@custom-variant` ([#19633](https://github.com/tailwindlabs/tailwindcss/pull/19633))
### Deprecated

View file

@ -4558,6 +4558,97 @@ describe('@custom-variant', () => {
`)
})
// https://github.com/tailwindlabs/tailwindcss/issues/19618
test('@custom-variant can use a @variant that eventually uses another @custom-variant', async () => {
expect(
await compileCss(
css`
@custom-variant a {
@slot;
}
@custom-variant b {
@variant a {
@slot;
}
}
@tailwind utilities;
`,
['a:flex', 'b:flex', 'a:b:flex', 'b:a:flex'],
),
).toMatchInlineSnapshot(`
".a\\:flex, .b\\:flex, .a\\:b\\:flex, .b\\:a\\:flex {
display: flex;
}"
`)
})
test('@custom-variant can use a @variant that eventually uses another @custom-variant (2)', async () => {
expect(
await compileCss(
css`
@custom-variant a {
.a {
@slot;
}
}
@custom-variant b {
.b {
@variant a {
.a-inside-b {
@slot;
}
}
}
}
@tailwind utilities;
`,
['a:flex', 'b:flex', 'a:b:flex', 'b:a:flex'],
),
).toMatchInlineSnapshot(`
".a\\:flex .a, .b\\:flex .b .a .a-inside-b, .a\\:b\\:flex .a .b .a .a-inside-b, .b\\:a\\:flex .b .a .a-inside-b .a {
display: flex;
}"
`)
})
// https://github.com/tailwindlabs/tailwindcss/issues/19618#issuecomment-3830775912
test('@custom-variant can use existing @slot @variants', async () => {
expect(
await compileCss(
css`
@custom-variant hocus {
@variant hover {
@variant focus {
@slot;
}
}
}
@custom-variant hover {
&:hover {
@slot;
}
&[data-hover] {
@slot;
}
}
@tailwind utilities;
`,
['hocus:flex'],
),
).toMatchInlineSnapshot(`
".hocus\\:flex:hover:focus, .hocus\\:flex[data-hover]:focus {
display: flex;
}"
`)
})
test('@custom-variant setup that results in a circular dependency error can be solved', async () => {
expect(
await compileCss(

View file

@ -1196,7 +1196,7 @@ export function substituteAtSlot(ast: AstNode[], nodes: AstNode[]) {
walk(ast, (node) => {
// Replace `@slot` with rule nodes
if (node.kind === 'at-rule' && node.name === '@slot') {
return WalkAction.Replace(nodes)
return WalkAction.ReplaceSkip(nodes)
}
// Wrap `@keyframes` and `@property` in `AtRoot` nodes