diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d59cd834..5fec361f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `@tailwindcss/vite` no longer forces a full page reload for external files (e.g.: `.php` files) ([#20414](https://github.com/tailwindlabs/tailwindcss/issues/20414)) - Canonicalization: don't merge utilities that reference different theme variables set to CSS-wide keywords like `unset` ([#20417](https://github.com/tailwindlabs/tailwindcss/pull/20417)) - 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)) +- 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)) ## [4.3.3] - 2026-07-16 diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index 56b069b31..454f4c1bf 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -1435,6 +1435,17 @@ test('supports', async () => { 'supports-[font-tech(color-COLRv1)]:flex', 'supports-[var(--test)]:flex', 'supports-[--test]:flex', + + // Only top-level and/or/not should have spaces around them. We should + // ignore and/or/not inside of `selector(…)` + 'supports-[selector(a:not(.foo))]:flex', + 'supports-[selector(a:is(.and,.or))]:flex', + 'supports-[(display:grid)or(display:flex)]:grid', + 'supports-[not(display:grid)]:flex', + 'supports-[foo-not(display:grid)]:flex', + 'supports-[selector([data-foo="("])or(display:grid)]:flex', + 'supports-[selector(.foo\\(bar)or(display:grid)]:flex', + 'supports-[((display:grid)or(display:flex))]:grid', ]), ).toMatchInlineSnapshot(` " @@ -1444,12 +1455,24 @@ test('supports', async () => { } } + @supports (display: grid) or (display: flex) { + .supports-\\[\\(\\(display\\:grid\\)or\\(display\\:flex\\)\\)\\]\\:grid { + display: grid; + } + } + @supports (display: grid) and font-format(opentype) { .supports-\\[\\(display\\:grid\\)_and_font-format\\(opentype\\)\\]\\:grid { display: grid; } } + @supports (display: grid) or (display: flex) { + .supports-\\[\\(display\\:grid\\)or\\(display\\:flex\\)\\]\\:grid { + display: grid; + } + } + @supports (--test: var(--tw)) { .supports-\\[--test\\]\\:flex { display: flex; @@ -1474,12 +1497,48 @@ test('supports', async () => { } } + @supports foo-not(display:grid) { + .supports-\\[foo-not\\(display\\:grid\\)\\]\\:flex { + display: flex; + } + } + + @supports not (display: grid) { + .supports-\\[not\\(display\\:grid\\)\\]\\:flex { + display: flex; + } + } + + @supports selector(.foo\\(bar) or (display: grid) { + .supports-\\[selector\\(\\.foo\\\\\\(bar\\)or\\(display\\:grid\\)\\]\\:flex { + display: flex; + } + } + @supports selector(A > B) { .supports-\\[selector\\(A_\\>_B\\)\\]\\:flex { display: flex; } } + @supports selector([data-foo="("]) or (display: grid) { + .supports-\\[selector\\(\\[data-foo\\=\\"\\(\\"\\]\\)or\\(display\\:grid\\)\\]\\:flex { + display: flex; + } + } + + @supports selector(a:is(.and,.or)) { + .supports-\\[selector\\(a\\:is\\(\\.and\\,\\.or\\)\\)\\]\\:flex { + display: flex; + } + } + + @supports selector(a:not(.foo)) { + .supports-\\[selector\\(a\\:not\\(\\.foo\\)\\)\\]\\:flex { + display: flex; + } + } + @supports var(--test) { .supports-\\[var\\(--test\\)\\]\\:flex { display: flex; diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 79ffe0162..c13c3ae12 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -938,7 +938,22 @@ export function createVariants(theme: Theme): Variants { if (/^[\w-]*\s*\(/.test(value)) { // Chrome has a bug where `(condition1)or(condition2)` is not valid, but // `(condition1) or (condition2)` is supported. - let query = value.replace(/\b(and|or|not)\b/g, ' $1 ') + let changed = false + let ast = ValueParser.parse(value) + walk(ast, (node) => { + if (node.kind !== 'function') return + + // Leave selectors as-is, they could contain `selector(a:not(b))`, and + // in this case we don't want the space around the `not`. + if (node.value === 'selector') return WalkAction.Skip + + if (node.value === 'and' || node.value === 'or' || node.value === 'not') { + changed = true + node.value = ` ${node.value} ` + } + }) + + let query = changed ? ValueParser.toCss(ast) : value ruleNode.nodes = [atRule('@supports', query, ruleNode.nodes)] return