Sort breakpoint variants with decimal values numerically (#20512)

## Summary

`compareBreakpoints` compared values in the same unit with `parseInt`,
which drops the fractional part, so `40.25rem` and `40.5rem` were
treated as equal. As a result `min-[40.5rem]:*` could be emitted before
`min-[40.25rem]:*` (the same happened for `@min-*` container queries),
which means the smaller breakpoint wins in the cascade. This switches
the comparison to `parseFloat` so decimal values are ordered
numerically; values that were already sorted correctly are unaffected.

## Test plan

- Added a test to `packages/tailwindcss/src/variants.test.ts` that sorts
`min-[40.25rem]`, `min-[40.5rem]`, `max-[40.25rem]` and `max-[40.5rem]`.
It fails without the change (the `min-[40.5rem]` rule is emitted before
`min-[40.25rem]`) and passes with it.
- `vitest run src/variants.test.ts -t "decimal values"` (in
`packages/tailwindcss`)
- `vitest run` (in `packages/tailwindcss`): 42 files, 5009 tests passed
- `prettier --check` on the changed files
This commit is contained in:
Alexandre Kohler 2026-09-25 08:07:37 -03:00 • committed by GitHub
parent 41d9cae8e5
commit 86f3daf912
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 76 additions and 1 deletions

View file

@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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))
- Don't warn about Angular's `::ng-deep` and `:host-context()` when optimizing CSS ([#20434](https://github.com/tailwindlabs/tailwindcss/pull/20434))
- 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))
- Sort `min-*`, `max-*`, and container query variants with decimal values numerically (e.g. `min-[40.25rem]` before `min-[40.5rem]`) ([#20512](https://github.com/tailwindlabs/tailwindcss/pull/20512))
## [4.3.3] - 2026-07-16

View file

@ -25,7 +25,7 @@ export function compareBreakpoints(a: string, z: string, direction: 'asc' | 'des
// Compare by bucket name
(aBucket === zBucket ? 0 : aBucket < zBucket ? -1 : 1) ||
// If bucket names are the same, compare by value
(direction === 'asc' ? parseInt(a) - parseInt(z) : parseInt(z) - parseInt(a))
(direction === 'asc' ? parseFloat(a) - parseFloat(z) : parseFloat(z) - parseFloat(a))
// If the groups are the same, and the contents are not numbers, the
// `order` will result in `NaN`. In this case, we want to make sorting

View file

@ -1424,6 +1424,80 @@ test('sorting `min` and `max` should sort by unit, then by value, then alphabeti
`)
})
test('sorting `min` and `max` should compare decimal values numerically', async () => {
expect(
await run([
'min-[40.5rem]:flex',
'min-[40.25rem]:flex',
'max-[40.25rem]:flex',
'max-[40.5rem]:flex',
]),
).toMatchInlineSnapshot(`
"
@media not all and (min-width: 40.5rem) {
.max-\\[40\\.5rem\\]\\:flex {
display: flex;
}
}
@media not all and (min-width: 40.25rem) {
.max-\\[40\\.25rem\\]\\:flex {
display: flex;
}
}
@media (min-width: 40.25rem) {
.min-\\[40\\.25rem\\]\\:flex {
display: flex;
}
}
@media (min-width: 40.5rem) {
.min-\\[40\\.5rem\\]\\:flex {
display: flex;
}
}
"
`)
})
test('sorting `@min` and `@max` should compare decimal values numerically', async () => {
expect(
await run([
'@min-[40.5rem]:flex',
'@min-[40.25rem]:flex',
'@max-[40.25rem]:flex',
'@max-[40.5rem]:flex',
]),
).toMatchInlineSnapshot(`
"
@container not (min-width: 40.5rem) {
.\\@max-\\[40\\.5rem\\]\\:flex {
display: flex;
}
}
@container not (min-width: 40.25rem) {
.\\@max-\\[40\\.25rem\\]\\:flex {
display: flex;
}
}
@container (min-width: 40.25rem) {
.\\@min-\\[40\\.25rem\\]\\:flex {
display: flex;
}
}
@container (min-width: 40.5rem) {
.\\@min-\\[40\\.5rem\\]\\:flex {
display: flex;
}
}
"
`)
})
test('supports', async () => {
expect(
await run([