Always produce <length> value when optimizing --spacing(0) (#20319)

This PR fixes a type issue when using `--spacing(0)`. This construction
doesn't really make sense, but if you use it, it produces the value `0`
instead of `calc(var(--spacing) * 0)` which is fine if you use it in a
spot where a `<length>` data type can be used, then the `0` is
interpreted as a `<length>`.

E.g.: `padding: 0` and `padding: 0px` are equivalent.

However, if you use it in a CSS variable, then the `0` will turn in a
`<number>` if you don't have an `@property` definition for that CSS
variable.

This on its own isn't the issue, but if you later use it as part of a
`calc(…)` then the `<number>` instead of `<length>` type is being used.
As seen in #20315.

We could remove the optimization, and use `calc(var(--spacing) * 0)`
again, but this is a bit silly since it only makes your CSS file larger.

We could try to be smart, and only do it _if_ we're assigning to a CSS
variable (which #20317 is doing). But if your CSS variable _does_ use a
`<length>` then it's a non-issue. We would run into the same issue if
you use `width: calc(100% --spacing(0))`. This value is a bit silly
anyway, but it would originally resolve to `calc(100% +
calc(var(--spacing) * 0))`, the optimization of `calc(100% - 0)` would
make it invalid, so the fix in #20317 would not be enough.

Instead I opted for an inbetween solution, by always using `0px`. In
most cases we can use `0`, but in the places we can't the `0px` would at
least ensure that we are dealing with `<length>` data types.

This way we don't have to try to be smart to analyze where we use the
value, and `0px` is still better than the long `calc(var(--spacing) *
0)` value.

Fixes: #20315
Closes: #20317

## Test plan

1. Manually tested that now `--spacing(0)` does produce `0px` which has
a length type
This commit is contained in:
Robin Malfait
2026-07-09 18:34:27 +02:00
committed by GitHub
parent 74b96c1481
commit 9183dbbed6
4 changed files with 44 additions and 33 deletions
+1
View File
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix stale `@tailwindcss/postcss` output when changed input CSS is passed from a preprocessor like Sass without updating the input file on disk ([#20310](https://github.com/tailwindlabs/tailwindcss/pull/20310))
- Ensure CSS nesting is handled even when Lightning CSS isn't run, like in `@tailwindcss/browser` and Tailwind Play ([#20124](https://github.com/tailwindlabs/tailwindcss/pull/20124))
- Prevent achromatic theme colors from shifting hue in `color-mix(…)` with polar color spaces like `oklch` ([#19830](https://github.com/tailwindlabs/tailwindcss/issues/19830))
- Ensure `--spacing(0)` is optimized to `0px` instead of `0` so it remains a `<length>` when used in `calc(…)` ([#20319](https://github.com/tailwindlabs/tailwindcss/pull/20319))
## [4.3.2] - 2026-06-26
+17 -9
View File
@@ -1,8 +1,9 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { describe, expect, test } from 'vitest'
import { compile } from '.'
import plugin from './plugin'
import { compileCss, run } from './test-utils/run'
import { compileCss, pretty, run } from './test-utils/run'
const css = String.raw
@@ -106,6 +107,14 @@ describe('--spacing(…)', () => {
})
describe('optimizations', () => {
// Use custom `compileCss` such that we can use a version without Lightning
// CSS. This way we test _our_ optimizations instead of Lightning's
// optimizations.
async function compileCss(css: string, options: Parameters<typeof compile>[1] = {}) {
let { build } = await compile(css, options)
return pretty(build([]))
}
test('--spacing(…) optimizes the output when the input is `0`', async () => {
expect(
await compileCss(css`
@@ -121,8 +130,8 @@ describe('--spacing(…)', () => {
).toMatchInlineSnapshot(`
"
.foo {
margin: 0;
padding: 0;
margin: 0px;
padding: 0px;
}
"
`)
@@ -143,8 +152,8 @@ describe('--spacing(…)', () => {
).toMatchInlineSnapshot(`
"
.foo {
margin: 0;
padding: 0;
margin: 0px;
padding: 0px;
}
"
`)
@@ -165,9 +174,8 @@ describe('--spacing(…)', () => {
).toMatchInlineSnapshot(`
"
:root, :host {
--spacing: .25rem;
--spacing: 0.25rem;
}
.foo {
margin: var(--spacing);
padding: var(--spacing);
@@ -191,8 +199,8 @@ describe('--spacing(…)', () => {
).toMatchInlineSnapshot(`
"
.foo {
margin: .25rem;
padding: .25rem;
margin: 0.25rem;
padding: 0.25rem;
}
"
`)
+4 -2
View File
@@ -70,11 +70,13 @@ function spacing(
// - We can assume that the `--spacing` value is not set to a `0`-like value.
// Otherwise `p-<anything>` would calculate as `0` which wouldn't make sense.
//
// - That means that a value of `0` can be replaced by `0`
// - That means that a value of `0` can be replaced by `0px`. It's important
// to keep the unit so the value stays a `<length>`, e.g. when assigned to
// a custom property and later used inside `calc(…)`.
// - That means that a value of `1` can be replaced by `multiplier`
let valueDimension = dimensions.get(value)
if (valueDimension) {
if (valueDimension[0] === 0) return '0'
if (valueDimension[0] === 0) return '0px'
if (valueDimension[0] === 1) return multiplier
}
+22 -22
View File
@@ -16145,7 +16145,7 @@ test('mask-t-from', async () => {
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
--tw-mask-top: linear-gradient(to top, var(--tw-mask-top-from-color) var(--tw-mask-top-from-position), var(--tw-mask-top-to-color) var(--tw-mask-top-to-position));
--tw-mask-top-from-position: 0;
--tw-mask-top-from-position: 0px;
-webkit-mask-composite: source-in;
-webkit-mask-composite: source-in;
mask-composite: intersect;
@@ -16405,7 +16405,7 @@ test('mask-t-to', async () => {
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
--tw-mask-top: linear-gradient(to top, var(--tw-mask-top-from-color) var(--tw-mask-top-from-position), var(--tw-mask-top-to-color) var(--tw-mask-top-to-position));
--tw-mask-top-to-position: 0;
--tw-mask-top-to-position: 0px;
-webkit-mask-composite: source-in;
-webkit-mask-composite: source-in;
mask-composite: intersect;
@@ -16666,7 +16666,7 @@ test('mask-r-from', async () => {
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
--tw-mask-right: linear-gradient(to right, var(--tw-mask-right-from-color) var(--tw-mask-right-from-position), var(--tw-mask-right-to-color) var(--tw-mask-right-to-position));
--tw-mask-right-from-position: 0;
--tw-mask-right-from-position: 0px;
-webkit-mask-composite: source-in;
-webkit-mask-composite: source-in;
mask-composite: intersect;
@@ -16927,7 +16927,7 @@ test('mask-r-to', async () => {
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
--tw-mask-right: linear-gradient(to right, var(--tw-mask-right-from-color) var(--tw-mask-right-from-position), var(--tw-mask-right-to-color) var(--tw-mask-right-to-position));
--tw-mask-right-to-position: 0;
--tw-mask-right-to-position: 0px;
-webkit-mask-composite: source-in;
-webkit-mask-composite: source-in;
mask-composite: intersect;
@@ -17188,7 +17188,7 @@ test('mask-b-from', async () => {
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
--tw-mask-bottom: linear-gradient(to bottom, var(--tw-mask-bottom-from-color) var(--tw-mask-bottom-from-position), var(--tw-mask-bottom-to-color) var(--tw-mask-bottom-to-position));
--tw-mask-bottom-from-position: 0;
--tw-mask-bottom-from-position: 0px;
-webkit-mask-composite: source-in;
-webkit-mask-composite: source-in;
mask-composite: intersect;
@@ -17449,7 +17449,7 @@ test('mask-b-to', async () => {
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
--tw-mask-bottom: linear-gradient(to bottom, var(--tw-mask-bottom-from-color) var(--tw-mask-bottom-from-position), var(--tw-mask-bottom-to-color) var(--tw-mask-bottom-to-position));
--tw-mask-bottom-to-position: 0;
--tw-mask-bottom-to-position: 0px;
-webkit-mask-composite: source-in;
-webkit-mask-composite: source-in;
mask-composite: intersect;
@@ -17710,7 +17710,7 @@ test('mask-l-from', async () => {
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
--tw-mask-left: linear-gradient(to left, var(--tw-mask-left-from-color) var(--tw-mask-left-from-position), var(--tw-mask-left-to-color) var(--tw-mask-left-to-position));
--tw-mask-left-from-position: 0;
--tw-mask-left-from-position: 0px;
-webkit-mask-composite: source-in;
-webkit-mask-composite: source-in;
mask-composite: intersect;
@@ -17971,7 +17971,7 @@ test('mask-l-to', async () => {
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
--tw-mask-left: linear-gradient(to left, var(--tw-mask-left-from-color) var(--tw-mask-left-from-position), var(--tw-mask-left-to-color) var(--tw-mask-left-to-position));
--tw-mask-left-to-position: 0;
--tw-mask-left-to-position: 0px;
-webkit-mask-composite: source-in;
-webkit-mask-composite: source-in;
mask-composite: intersect;
@@ -18240,9 +18240,9 @@ test('mask-x-from', async () => {
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
--tw-mask-right: linear-gradient(to right, var(--tw-mask-right-from-color) var(--tw-mask-right-from-position), var(--tw-mask-right-to-color) var(--tw-mask-right-to-position));
--tw-mask-right-from-position: 0;
--tw-mask-right-from-position: 0px;
--tw-mask-left: linear-gradient(to left, var(--tw-mask-left-from-color) var(--tw-mask-left-from-position), var(--tw-mask-left-to-color) var(--tw-mask-left-to-position));
--tw-mask-left-from-position: 0;
--tw-mask-left-from-position: 0px;
-webkit-mask-composite: source-in;
-webkit-mask-composite: source-in;
mask-composite: intersect;
@@ -18547,9 +18547,9 @@ test('mask-x-to', async () => {
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
--tw-mask-right: linear-gradient(to right, var(--tw-mask-right-from-color) var(--tw-mask-right-from-position), var(--tw-mask-right-to-color) var(--tw-mask-right-to-position));
--tw-mask-right-to-position: 0;
--tw-mask-right-to-position: 0px;
--tw-mask-left: linear-gradient(to left, var(--tw-mask-left-from-color) var(--tw-mask-left-from-position), var(--tw-mask-left-to-color) var(--tw-mask-left-to-position));
--tw-mask-left-to-position: 0;
--tw-mask-left-to-position: 0px;
-webkit-mask-composite: source-in;
-webkit-mask-composite: source-in;
mask-composite: intersect;
@@ -18854,9 +18854,9 @@ test('mask-y-from', async () => {
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
--tw-mask-top: linear-gradient(to top, var(--tw-mask-top-from-color) var(--tw-mask-top-from-position), var(--tw-mask-top-to-color) var(--tw-mask-top-to-position));
--tw-mask-top-from-position: 0;
--tw-mask-top-from-position: 0px;
--tw-mask-bottom: linear-gradient(to bottom, var(--tw-mask-bottom-from-color) var(--tw-mask-bottom-from-position), var(--tw-mask-bottom-to-color) var(--tw-mask-bottom-to-position));
--tw-mask-bottom-from-position: 0;
--tw-mask-bottom-from-position: 0px;
-webkit-mask-composite: source-in;
-webkit-mask-composite: source-in;
mask-composite: intersect;
@@ -19161,9 +19161,9 @@ test('mask-y-to', async () => {
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
--tw-mask-top: linear-gradient(to top, var(--tw-mask-top-from-color) var(--tw-mask-top-from-position), var(--tw-mask-top-to-color) var(--tw-mask-top-to-position));
--tw-mask-top-to-position: 0;
--tw-mask-top-to-position: 0px;
--tw-mask-bottom: linear-gradient(to bottom, var(--tw-mask-bottom-from-color) var(--tw-mask-bottom-from-position), var(--tw-mask-bottom-to-color) var(--tw-mask-bottom-to-position));
--tw-mask-bottom-to-position: 0;
--tw-mask-bottom-to-position: 0px;
-webkit-mask-composite: source-in;
-webkit-mask-composite: source-in;
mask-composite: intersect;
@@ -19618,7 +19618,7 @@ test('mask-linear-from', async () => {
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
--tw-mask-linear-stops: var(--tw-mask-linear-position), var(--tw-mask-linear-from-color) var(--tw-mask-linear-from-position), var(--tw-mask-linear-to-color) var(--tw-mask-linear-to-position);
--tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops));
--tw-mask-linear-from-position: 0;
--tw-mask-linear-from-position: 0px;
-webkit-mask-composite: source-in;
-webkit-mask-composite: source-in;
mask-composite: intersect;
@@ -19858,7 +19858,7 @@ test('mask-linear-to', async () => {
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
--tw-mask-linear-stops: var(--tw-mask-linear-position), var(--tw-mask-linear-from-color) var(--tw-mask-linear-from-position), var(--tw-mask-linear-to-color) var(--tw-mask-linear-to-position);
--tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops));
--tw-mask-linear-to-position: 0;
--tw-mask-linear-to-position: 0px;
-webkit-mask-composite: source-in;
-webkit-mask-composite: source-in;
mask-composite: intersect;
@@ -20347,7 +20347,7 @@ test('mask-radial-from', async () => {
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
--tw-mask-radial-stops: var(--tw-mask-radial-shape) var(--tw-mask-radial-size) at var(--tw-mask-radial-position), var(--tw-mask-radial-from-color) var(--tw-mask-radial-from-position), var(--tw-mask-radial-to-color) var(--tw-mask-radial-to-position);
--tw-mask-radial: radial-gradient(var(--tw-mask-radial-stops));
--tw-mask-radial-from-position: 0;
--tw-mask-radial-from-position: 0px;
-webkit-mask-composite: source-in;
-webkit-mask-composite: source-in;
mask-composite: intersect;
@@ -20601,7 +20601,7 @@ test('mask-radial-to', async () => {
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
--tw-mask-radial-stops: var(--tw-mask-radial-shape) var(--tw-mask-radial-size) at var(--tw-mask-radial-position), var(--tw-mask-radial-from-color) var(--tw-mask-radial-from-position), var(--tw-mask-radial-to-color) var(--tw-mask-radial-to-position);
--tw-mask-radial: radial-gradient(var(--tw-mask-radial-stops));
--tw-mask-radial-to-position: 0;
--tw-mask-radial-to-position: 0px;
-webkit-mask-composite: source-in;
-webkit-mask-composite: source-in;
mask-composite: intersect;
@@ -21014,7 +21014,7 @@ test('mask-conic-from', async () => {
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
--tw-mask-conic-stops: from var(--tw-mask-conic-position), var(--tw-mask-conic-from-color) var(--tw-mask-conic-from-position), var(--tw-mask-conic-to-color) var(--tw-mask-conic-to-position);
--tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops));
--tw-mask-conic-from-position: 0;
--tw-mask-conic-from-position: 0px;
-webkit-mask-composite: source-in;
-webkit-mask-composite: source-in;
mask-composite: intersect;
@@ -21254,7 +21254,7 @@ test('mask-conic-to', async () => {
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
--tw-mask-conic-stops: from var(--tw-mask-conic-position), var(--tw-mask-conic-from-color) var(--tw-mask-conic-from-position), var(--tw-mask-conic-to-color) var(--tw-mask-conic-to-position);
--tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops));
--tw-mask-conic-to-position: 0;
--tw-mask-conic-to-position: 0px;
-webkit-mask-composite: source-in;
-webkit-mask-composite: source-in;
mask-composite: intersect;