diff --git a/CHANGELOG.md b/CHANGELOG.md index 65a121f9fd7d..1b9081231899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Nothing yet! +### Fixed + +- Ensure custom variants using `@scope` wrap the generated utilities instead of nesting inside them ([#20344](https://github.com/tailwindlabs/tailwindcss/pull/20344)) ## [4.3.3] - 2026-07-16 diff --git a/packages/tailwindcss/src/ast.test.ts b/packages/tailwindcss/src/ast.test.ts index f3499b350752..d6db07174d37 100644 --- a/packages/tailwindcss/src/ast.test.ts +++ b/packages/tailwindcss/src/ast.test.ts @@ -1093,6 +1093,30 @@ describe('optimization', () => { `) }) + it('should hoist `@scope` at-rules', async () => { + expect( + optimize(css` + @layer utilities { + .a { + @scope (.b) to (.c) { + --x: 1; + } + } + } + `), + ).toMatchInlineSnapshot(` + " + @layer utilities { + @scope (.b) to (.c) { + .a { + --x: 1; + } + } + } + " + `) + }) + it('should leave `@property` and `@apply` alone', async () => { expect( optimize(css` diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index e22c13e83900..8777de06e5ba 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -1311,6 +1311,7 @@ const HOISTABLE_AT_RULES = new Set([ '@layer', '@media', '@page', + '@scope', '@starting-style', '@supports', '@view-transition', @@ -1323,6 +1324,7 @@ const DROPPABLE_IF_EMPTY_AT_RULES = new Set([ '@container', '@media', '@page', + '@scope', '@starting-style', '@supports', '@view-transition', diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 2a72336fb738..fe76d39075d1 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -4683,6 +4683,64 @@ describe('@custom-variant', () => { " `) }) + + test('@scope with @slot', async () => { + expect( + await run( + ['in-checkout:underline'], + css` + @custom-variant in-checkout { + @scope (.checkout) { + @slot; + } + } + + @layer utilities { + @tailwind utilities; + } + `, + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { + @scope (.checkout) { + .in-checkout\\:underline { + text-decoration-line: underline; + } + } + } + " + `) + }) + + test('@scope with a limit with @slot', async () => { + expect( + await run( + ['in-checkout:underline'], + css` + @custom-variant in-checkout { + @scope (.checkout) to (.payment) { + @slot; + } + } + + @layer utilities { + @tailwind utilities; + } + `, + ), + ).toMatchInlineSnapshot(` + " + @layer utilities { + @scope (.checkout) to (.payment) { + .in-checkout\\:underline { + text-decoration-line: underline; + } + } + } + " + `) + }) }) test('built-in variants can be overridden while keeping their order', async () => {