Don't space out and/or/not inside function calls in supports-[…] variants (#20420)

## Summary

The `supports-[…]` variant works around a Chrome bug where `@supports
(a)or(b)`
is invalid by spacing out the `and`, `or`, and `not` keywords. However,
the
replacement is applied to the entire value, including the inside of
function
calls, where these words can be part of a selector.

For example, `supports-[selector(a:not(.foo))]:flex` generates:

```css
@supports selector(a: not (.foo))
```

The selector `a: not (.foo)` is unparsable, so a condition that is true
in
every browser silently becomes false and the utility never applies. The
same
happens to class names like `.and` or `.or` inside `selector(…)`.

This PR only spaces out the keywords at the condition level: parens
preceded by
an identifier (other than the keywords themselves) start a function
call, and
everything inside is left as-is. The Chrome workaround still applies to
the
condition itself, e.g. `supports-[(display:grid)or(display:flex)]` still
becomes `@supports (display: grid) or (display: flex)`.

## Test plan

- Added a test covering `selector(a:not(.foo))`, class names
`.and`/`.or`
inside `selector(…)`, the Chrome `(a)or(b)` workaround, and a top-level
  `not(…)` condition.
- `pnpm vitest run packages/tailwindcss/src/variants.test.ts` — 102
passed.

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
Srinu desetti 2026-08-15 01:13:33 +05:30 • committed by GitHub
parent 7a7f386648
commit 90f8ff41c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 76 additions and 1 deletions

View file

@ -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

View file

@ -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;

View file

@ -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