From f74e2130444b1bfff4b287ac02c790a677ebff66 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 3 Jun 2026 15:33:05 +0300 Subject: [PATCH 01/10] Avoid generating useless multiplications by zero --- CHANGELOG.md | 4 + packages/tailwindcss/src/utilities.test.ts | 187 +++++++++++++++++--- packages/tailwindcss/src/utilities.ts | 192 ++++++++++++++------- 3 files changed, 292 insertions(+), 91 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 759bf95be7c1..c31e587d0113 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `--silent` option to suppress output in `@tailwindcss/cli` ([#20100](https://github.com/tailwindlabs/tailwindcss/pull/20100)) +### Changed + +- Skip redundant `calc(…)` for utilities whose computed value is zero (e.g. `m-0`, `px-0`, `left-0`, `space-x-0`, `divide-x-0`, `text-sm/0`, `mask-linear-0`) + ### Fixed - Remove deprecation warnings by using `Module#registerHooks` instead of `Module#register` on Node 26+ ([#20028](https://github.com/tailwindlabs/tailwindcss/pull/20028)) diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index cefcf7d03b92..dcf6fb88d49c 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -318,6 +318,81 @@ test('inset', async () => { ).toEqual('') }) +test('inset/position utilities with a `0` value emit a constant, not calc()', async () => { + let output = await run( + [ + 'inset-0', + 'inset-x-0', + 'inset-y-0', + 'inset-s-0', + 'inset-e-0', + 'inset-bs-0', + 'inset-be-0', + 'top-0', + 'right-0', + 'bottom-0', + 'left-0', + // Negative zero must collapse too (no `calc(0 * -1)`). + '-inset-0', + '-top-0', + ], + css` + @theme { + --spacing: 0.25rem; + } + @tailwind utilities; + `, + ) + + expect(output).toMatchInlineSnapshot(` + " + .-inset-0, .inset-0 { + inset: 0; + } + + .inset-x-0 { + inset-inline: 0; + } + + .inset-y-0 { + inset-block: 0; + } + + .inset-s-0 { + inset-inline-start: 0; + } + + .inset-e-0 { + inset-inline-end: 0; + } + + .inset-bs-0 { + inset-block-start: 0; + } + + .inset-be-0 { + inset-block-end: 0; + } + + .-top-0, .top-0 { + top: 0; + } + + .right-0 { + right: 0; + } + + .bottom-0 { + bottom: 0; + } + + .left-0 { + left: 0; + } + " + `) +}) + test('inset-x', async () => { expect( await run( @@ -9922,9 +9997,10 @@ test('gap-y', async () => { test('space-x', async () => { expect( await run( - ['space-x-4', 'space-x-[4px]', '-space-x-4'], + ['space-x-0', 'space-x-4', 'space-x-[4px]', '-space-x-4'], css` @theme { + --spacing: 0.25rem; --spacing-4: 1rem; } @tailwind utilities; @@ -9962,6 +10038,10 @@ test('space-x', async () => { margin-inline-end: calc(4px * calc(1 - var(--tw-space-x-reverse))); } + :where(.space-x-0 > :not(:last-child)) { + margin-inline: 0; + } + @property --tw-space-x-reverse { syntax: "*"; inherits: false; @@ -9975,9 +10055,10 @@ test('space-x', async () => { test('space-y', async () => { expect( await run( - ['space-y-4', 'space-y-[4px]', '-space-y-4'], + ['space-y-0', 'space-y-4', 'space-y-[4px]', '-space-y-4'], css` @theme { + --spacing: 0.25rem; --spacing-4: 1rem; } @tailwind utilities; @@ -10015,6 +10096,10 @@ test('space-y', async () => { margin-block-end: calc(4px * calc(1 - var(--tw-space-y-reverse))); } + :where(.space-y-0 > :not(:last-child)) { + margin-block: 0; + } + @property --tw-space-y-reverse { syntax: "*"; inherits: false; @@ -10076,7 +10161,7 @@ test('space-y-reverse', async () => { }) test('divide-x', async () => { - expect(await run(['divide-x', 'divide-x-4', 'divide-x-123', 'divide-x-[4px]'])) + expect(await run(['divide-x', 'divide-x-0', 'divide-x-4', 'divide-x-123', 'divide-x-[4px]'])) .toMatchInlineSnapshot(` " @layer properties { @@ -10116,6 +10201,11 @@ test('divide-x', async () => { border-inline-end-width: calc(4px * calc(1 - var(--tw-divide-x-reverse))); } + :where(.divide-x-0 > :not(:last-child)) { + border-inline-style: var(--tw-border-style); + border-inline-width: 0; + } + @property --tw-divide-x-reverse { syntax: "*"; inherits: false; @@ -10190,7 +10280,7 @@ test('divide-x with custom default border width', async () => { }) test('divide-y', async () => { - expect(await run(['divide-y', 'divide-y-4', 'divide-y-123', 'divide-y-[4px]'])) + expect(await run(['divide-y', 'divide-y-0', 'divide-y-4', 'divide-y-123', 'divide-y-[4px]'])) .toMatchInlineSnapshot(` " @layer properties { @@ -10234,6 +10324,13 @@ test('divide-y', async () => { border-bottom-width: calc(4px * calc(1 - var(--tw-divide-y-reverse))); } + :where(.divide-y-0 > :not(:last-child)) { + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: 0; + border-bottom-width: 0; + } + @property --tw-divide-y-reverse { syntax: "*"; inherits: false; @@ -16014,7 +16111,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: calc(var(--spacing) * 0); + --tw-mask-top-from-position: 0; -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -16274,7 +16371,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: calc(var(--spacing) * 0); + --tw-mask-top-to-position: 0; -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -16535,7 +16632,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: calc(var(--spacing) * 0); + --tw-mask-right-from-position: 0; -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -16796,7 +16893,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: calc(var(--spacing) * 0); + --tw-mask-right-to-position: 0; -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -17057,7 +17154,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: calc(var(--spacing) * 0); + --tw-mask-bottom-from-position: 0; -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -17318,7 +17415,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: calc(var(--spacing) * 0); + --tw-mask-bottom-to-position: 0; -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -17579,7 +17676,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: calc(var(--spacing) * 0); + --tw-mask-left-from-position: 0; -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -17840,7 +17937,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: calc(var(--spacing) * 0); + --tw-mask-left-to-position: 0; -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -18109,9 +18206,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: calc(var(--spacing) * 0); + --tw-mask-right-from-position: 0; --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: calc(var(--spacing) * 0); + --tw-mask-left-from-position: 0; -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -18416,9 +18513,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: calc(var(--spacing) * 0); + --tw-mask-right-to-position: 0; --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: calc(var(--spacing) * 0); + --tw-mask-left-to-position: 0; -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -18723,9 +18820,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: calc(var(--spacing) * 0); + --tw-mask-top-from-position: 0; --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: calc(var(--spacing) * 0); + --tw-mask-bottom-from-position: 0; -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -19030,9 +19127,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: calc(var(--spacing) * 0); + --tw-mask-top-to-position: 0; --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: calc(var(--spacing) * 0); + --tw-mask-bottom-to-position: 0; -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -19251,7 +19348,7 @@ test('mask-y-to', async () => { }) test('mask-linear', async () => { - expect(await run(['mask-linear-45', 'mask-linear-[3rad]', '-mask-linear-45'])) + expect(await run(['mask-linear-0', 'mask-linear-45', 'mask-linear-[3rad]', '-mask-linear-45'])) .toMatchInlineSnapshot(` " @layer properties { @@ -19280,6 +19377,17 @@ test('mask-linear', async () => { mask-composite: intersect; } + .mask-linear-0 { + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + --tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops, var(--tw-mask-linear-position))); + --tw-mask-linear-position: 0deg; + -webkit-mask-composite: source-in; + -webkit-mask-composite: source-in; + mask-composite: intersect; + } + .mask-linear-45 { -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); @@ -19446,7 +19554,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: calc(var(--spacing) * 0); + --tw-mask-linear-from-position: 0; -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -19686,7 +19794,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: calc(var(--spacing) * 0); + --tw-mask-linear-to-position: 0; -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -20175,7 +20283,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: calc(var(--spacing) * 0); + --tw-mask-radial-from-position: 0; -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -20429,7 +20537,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: calc(var(--spacing) * 0); + --tw-mask-radial-to-position: 0; -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -20606,7 +20714,7 @@ test('mask-radial-to', async () => { }) test('mask-conic', async () => { - expect(await run(['mask-conic-45', 'mask-conic-[3rad]', '-mask-conic-45'])) + expect(await run(['mask-conic-0', 'mask-conic-45', 'mask-conic-[3rad]', '-mask-conic-45'])) .toMatchInlineSnapshot(` " @layer properties { @@ -20635,6 +20743,17 @@ test('mask-conic', async () => { mask-composite: intersect; } + .mask-conic-0 { + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + --tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops, var(--tw-mask-conic-position))); + --tw-mask-conic-position: 0deg; + -webkit-mask-composite: source-in; + -webkit-mask-composite: source-in; + mask-composite: intersect; + } + .mask-conic-45 { -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); @@ -20801,7 +20920,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: calc(var(--spacing) * 0); + --tw-mask-conic-from-position: 0; -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -21041,7 +21160,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: calc(var(--spacing) * 0); + --tw-mask-conic-to-position: 0; -webkit-mask-composite: source-in; -webkit-mask-composite: source-in; mask-composite: intersect; @@ -26308,12 +26427,14 @@ test('text', async () => { // font-size / line-height / letter-spacing / font-weight 'text-sm', + 'text-sm/0', 'text-sm/6', 'text-sm/none', 'text-[10px]/none', 'text-sm/snug', 'text-sm/[4px]', 'text-[12px]', + 'text-[12px]/0', 'text-[12px]/6', 'text-[50%]', 'text-[50%]/6', @@ -26357,6 +26478,11 @@ test('text', async () => { line-height: 1; } + .text-\\[12px\\]\\/0 { + font-size: 12px; + line-height: 0; + } + .text-\\[12px\\]\\/6 { font-size: 12px; line-height: calc(var(--spacing) * 6); @@ -26387,6 +26513,11 @@ test('text', async () => { line-height: var(--tw-leading, var(--text-sm--line-height)); } + .text-sm\\/0 { + font-size: var(--text-sm); + line-height: 0; + } + .text-sm\\/6 { font-size: var(--text-sm); line-height: calc(var(--spacing) * 6); diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 724449ca9566..cfeb4b40dfce 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -550,6 +550,7 @@ export function createUtilities(theme: Theme) { if (!multiplier) return null if (!isValidSpacingMultiplier(value)) return null + if (Number(value) === 0) return '0' return `calc(${multiplier} * ${value})` }, handleNegativeBareValue: ({ value }) => { @@ -557,6 +558,7 @@ export function createUtilities(theme: Theme) { if (!multiplier) return null if (!isValidSpacingMultiplier(value)) return null + if (Number(value) === 0) return '0' return `calc(${multiplier} * -${value})` }, handle, @@ -2119,31 +2121,51 @@ export function createUtilities(theme: Theme) { spacingUtility( 'space-x', ['--space', '--spacing'], - (value) => [ - atRoot([property('--tw-space-x-reverse', '0')]), - - styleRule(':where(& > :not(:last-child))', [ - decl('--tw-sort', 'row-gap'), - decl('--tw-space-x-reverse', '0'), - decl('margin-inline-start', `calc(${value} * var(--tw-space-x-reverse))`), - decl('margin-inline-end', `calc(${value} * calc(1 - var(--tw-space-x-reverse)))`), - ]), - ], + (value) => + // Skip the variable entirely if we would multiply it by zero. + value === '0' + ? [ + styleRule(':where(& > :not(:last-child))', [ + decl('--tw-sort', 'row-gap'), + decl('margin-inline-start', '0'), + decl('margin-inline-end', '0'), + ]), + ] + : [ + atRoot([property('--tw-space-x-reverse', '0')]), + + styleRule(':where(& > :not(:last-child))', [ + decl('--tw-sort', 'row-gap'), + decl('--tw-space-x-reverse', '0'), + decl('margin-inline-start', `calc(${value} * var(--tw-space-x-reverse))`), + decl('margin-inline-end', `calc(${value} * calc(1 - var(--tw-space-x-reverse)))`), + ]), + ], { supportsNegative: true }, ) spacingUtility( 'space-y', ['--space', '--spacing'], - (value) => [ - atRoot([property('--tw-space-y-reverse', '0')]), - styleRule(':where(& > :not(:last-child))', [ - decl('--tw-sort', 'column-gap'), - decl('--tw-space-y-reverse', '0'), - decl('margin-block-start', `calc(${value} * var(--tw-space-y-reverse))`), - decl('margin-block-end', `calc(${value} * calc(1 - var(--tw-space-y-reverse)))`), - ]), - ], + (value) => + // Skip the variable entirely if we would multiply it by zero. + value === '0' + ? [ + styleRule(':where(& > :not(:last-child))', [ + decl('--tw-sort', 'column-gap'), + decl('margin-block-start', '0'), + decl('margin-block-end', '0'), + ]), + ] + : [ + atRoot([property('--tw-space-y-reverse', '0')]), + styleRule(':where(& > :not(:last-child))', [ + decl('--tw-sort', 'column-gap'), + decl('--tw-space-y-reverse', '0'), + decl('margin-block-start', `calc(${value} * var(--tw-space-y-reverse))`), + decl('margin-block-end', `calc(${value} * calc(1 - var(--tw-space-y-reverse)))`), + ]), + ], { supportsNegative: true }, ) @@ -2549,18 +2571,33 @@ export function createUtilities(theme: Theme) { if (!isPositiveInteger(value)) return null return `${value}px` }, - handle: (value) => [ - atRoot([property('--tw-divide-x-reverse', '0')]), - - styleRule(':where(& > :not(:last-child))', [ - decl('--tw-sort', 'divide-x-width'), - borderProperties(), - decl('--tw-divide-x-reverse', '0'), - decl('border-inline-style', 'var(--tw-border-style)'), - decl('border-inline-start-width', `calc(${value} * var(--tw-divide-x-reverse))`), - decl('border-inline-end-width', `calc(${value} * calc(1 - var(--tw-divide-x-reverse)))`), - ]), - ], + handle: (value) => + // Skip the variable entirely if we would multiply it by zero. + value === '0' || value === '0px' + ? [ + styleRule(':where(& > :not(:last-child))', [ + decl('--tw-sort', 'divide-x-width'), + borderProperties(), + decl('border-inline-style', 'var(--tw-border-style)'), + decl('border-inline-start-width', '0'), + decl('border-inline-end-width', '0'), + ]), + ] + : [ + atRoot([property('--tw-divide-x-reverse', '0')]), + + styleRule(':where(& > :not(:last-child))', [ + decl('--tw-sort', 'divide-x-width'), + borderProperties(), + decl('--tw-divide-x-reverse', '0'), + decl('border-inline-style', 'var(--tw-border-style)'), + decl('border-inline-start-width', `calc(${value} * var(--tw-divide-x-reverse))`), + decl( + 'border-inline-end-width', + `calc(${value} * calc(1 - var(--tw-divide-x-reverse)))`, + ), + ]), + ], }) functionalUtility('divide-y', { @@ -2570,19 +2607,36 @@ export function createUtilities(theme: Theme) { if (!isPositiveInteger(value)) return null return `${value}px` }, - handle: (value) => [ - atRoot([property('--tw-divide-y-reverse', '0')]), - - styleRule(':where(& > :not(:last-child))', [ - decl('--tw-sort', 'divide-y-width'), - borderProperties(), - decl('--tw-divide-y-reverse', '0'), - decl('border-bottom-style', 'var(--tw-border-style)'), - decl('border-top-style', 'var(--tw-border-style)'), - decl('border-top-width', `calc(${value} * var(--tw-divide-y-reverse))`), - decl('border-bottom-width', `calc(${value} * calc(1 - var(--tw-divide-y-reverse)))`), - ]), - ], + handle: (value) => + // A zero border width resolves to zero regardless of the reverse + // direction, so we can skip the reverse variable entirely. + value === '0' || value === '0px' + ? [ + styleRule(':where(& > :not(:last-child))', [ + decl('--tw-sort', 'divide-y-width'), + borderProperties(), + decl('border-bottom-style', 'var(--tw-border-style)'), + decl('border-top-style', 'var(--tw-border-style)'), + decl('border-top-width', '0'), + decl('border-bottom-width', '0'), + ]), + ] + : [ + atRoot([property('--tw-divide-y-reverse', '0')]), + + styleRule(':where(& > :not(:last-child))', [ + decl('--tw-sort', 'divide-y-width'), + borderProperties(), + decl('--tw-divide-y-reverse', '0'), + decl('border-bottom-style', 'var(--tw-border-style)'), + decl('border-top-style', 'var(--tw-border-style)'), + decl('border-top-width', `calc(${value} * var(--tw-divide-y-reverse))`), + decl( + 'border-bottom-width', + `calc(${value} * calc(1 - var(--tw-divide-y-reverse)))`, + ), + ]), + ], }) suggest('divide-x', () => [ @@ -3232,21 +3286,23 @@ export function createUtilities(theme: Theme) { { if (candidate.modifier) return - let type = inferDataType(candidate.value.value, ['number', 'percentage']) + let value = candidate.value.value + let type = inferDataType(value, ['number', 'percentage']) if (!type) return switch (type) { case 'number': { let multiplier = theme.resolve(null, ['--spacing']) if (!multiplier) return - if (!isValidSpacingMultiplier(candidate.value.value)) return + if (!isValidSpacingMultiplier(value)) return - return desc.position(`calc(${multiplier} * ${candidate.value.value})`) + if (Number(value) === 0) return desc.position('0') + return desc.position(`calc(${multiplier} * ${value})`) } case 'percentage': { - if (!isPositiveInteger(candidate.value.value.slice(0, -1))) return - return desc.position(candidate.value.value) + if (!isPositiveInteger(value.slice(0, -1))) return + return desc.position(value) } default: { @@ -3416,13 +3472,15 @@ export function createUtilities(theme: Theme) { defaultValue: null, supportsNegative: true, supportsFractions: false, - handleBareValue(value) { - if (!isPositiveInteger(value.value)) return null - return `calc(1deg * ${value.value})` + handleBareValue({ value }) { + if (!isPositiveInteger(value)) return null + if (Number(value) === 0) return '0deg' + return `calc(1deg * ${value})` }, - handleNegativeBareValue(value) { - if (!isPositiveInteger(value.value)) return null - return `calc(1deg * -${value.value})` + handleNegativeBareValue({ value }) { + if (!isPositiveInteger(value)) return null + if (Number(value) === 0) return '0deg' + return `calc(1deg * -${value})` }, handle: (value) => [ maskPropertiesGradient(), @@ -3634,13 +3692,15 @@ export function createUtilities(theme: Theme) { defaultValue: null, supportsNegative: true, supportsFractions: false, - handleBareValue(value) { - if (!isPositiveInteger(value.value)) return null - return `calc(1deg * ${value.value})` + handleBareValue({ value }) { + if (!isPositiveInteger(value)) return null + if (Number(value) === 0) return '0deg' + return `calc(1deg * ${value})` }, - handleNegativeBareValue(value) { - if (!isPositiveInteger(value.value)) return null - return `calc(1deg * -${value.value})` + handleNegativeBareValue({ value }) { + if (!isPositiveInteger(value)) return null + if (Number(value) === 0) return '0deg' + return `calc(1deg * -${value})` }, handle: (value) => [ maskPropertiesGradient(), @@ -5227,7 +5287,10 @@ export function createUtilities(theme: Theme) { if (!modifier && isValidSpacingMultiplier(candidate.modifier.value)) { let multiplier = theme.resolve(null, ['--spacing']) if (!multiplier) return null - modifier = `calc(${multiplier} * ${candidate.modifier.value})` + modifier = + Number(candidate.modifier.value) === 0 + ? '0' + : `calc(${multiplier} * ${candidate.modifier.value})` } // Shorthand for `leading-none` @@ -5280,7 +5343,10 @@ export function createUtilities(theme: Theme) { if (!modifier && isValidSpacingMultiplier(candidate.modifier.value)) { let multiplier = theme.resolve(null, ['--spacing']) if (!multiplier) return null - modifier = `calc(${multiplier} * ${candidate.modifier.value})` + modifier = + Number(candidate.modifier.value) === 0 + ? '0' + : `calc(${multiplier} * ${candidate.modifier.value})` } // Shorthand for `leading-none` From c6b2b0e90408398e05c9f973eb1bd0f374441fbb Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 3 Jun 2026 15:32:54 +0200 Subject: [PATCH 02/10] revert: do not handle this manually --- packages/tailwindcss/src/utilities.ts | 164 ++++++++------------------ 1 file changed, 51 insertions(+), 113 deletions(-) diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index cfeb4b40dfce..b964ecc9310a 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -550,7 +550,6 @@ export function createUtilities(theme: Theme) { if (!multiplier) return null if (!isValidSpacingMultiplier(value)) return null - if (Number(value) === 0) return '0' return `calc(${multiplier} * ${value})` }, handleNegativeBareValue: ({ value }) => { @@ -558,7 +557,6 @@ export function createUtilities(theme: Theme) { if (!multiplier) return null if (!isValidSpacingMultiplier(value)) return null - if (Number(value) === 0) return '0' return `calc(${multiplier} * -${value})` }, handle, @@ -2121,51 +2119,31 @@ export function createUtilities(theme: Theme) { spacingUtility( 'space-x', ['--space', '--spacing'], - (value) => - // Skip the variable entirely if we would multiply it by zero. - value === '0' - ? [ - styleRule(':where(& > :not(:last-child))', [ - decl('--tw-sort', 'row-gap'), - decl('margin-inline-start', '0'), - decl('margin-inline-end', '0'), - ]), - ] - : [ - atRoot([property('--tw-space-x-reverse', '0')]), - - styleRule(':where(& > :not(:last-child))', [ - decl('--tw-sort', 'row-gap'), - decl('--tw-space-x-reverse', '0'), - decl('margin-inline-start', `calc(${value} * var(--tw-space-x-reverse))`), - decl('margin-inline-end', `calc(${value} * calc(1 - var(--tw-space-x-reverse)))`), - ]), - ], + (value) => [ + atRoot([property('--tw-space-x-reverse', '0')]), + + styleRule(':where(& > :not(:last-child))', [ + decl('--tw-sort', 'row-gap'), + decl('--tw-space-x-reverse', '0'), + decl('margin-inline-start', `calc(${value} * var(--tw-space-x-reverse))`), + decl('margin-inline-end', `calc(${value} * calc(1 - var(--tw-space-x-reverse)))`), + ]), + ], { supportsNegative: true }, ) spacingUtility( 'space-y', ['--space', '--spacing'], - (value) => - // Skip the variable entirely if we would multiply it by zero. - value === '0' - ? [ - styleRule(':where(& > :not(:last-child))', [ - decl('--tw-sort', 'column-gap'), - decl('margin-block-start', '0'), - decl('margin-block-end', '0'), - ]), - ] - : [ - atRoot([property('--tw-space-y-reverse', '0')]), - styleRule(':where(& > :not(:last-child))', [ - decl('--tw-sort', 'column-gap'), - decl('--tw-space-y-reverse', '0'), - decl('margin-block-start', `calc(${value} * var(--tw-space-y-reverse))`), - decl('margin-block-end', `calc(${value} * calc(1 - var(--tw-space-y-reverse)))`), - ]), - ], + (value) => [ + atRoot([property('--tw-space-y-reverse', '0')]), + styleRule(':where(& > :not(:last-child))', [ + decl('--tw-sort', 'column-gap'), + decl('--tw-space-y-reverse', '0'), + decl('margin-block-start', `calc(${value} * var(--tw-space-y-reverse))`), + decl('margin-block-end', `calc(${value} * calc(1 - var(--tw-space-y-reverse)))`), + ]), + ], { supportsNegative: true }, ) @@ -2571,33 +2549,18 @@ export function createUtilities(theme: Theme) { if (!isPositiveInteger(value)) return null return `${value}px` }, - handle: (value) => - // Skip the variable entirely if we would multiply it by zero. - value === '0' || value === '0px' - ? [ - styleRule(':where(& > :not(:last-child))', [ - decl('--tw-sort', 'divide-x-width'), - borderProperties(), - decl('border-inline-style', 'var(--tw-border-style)'), - decl('border-inline-start-width', '0'), - decl('border-inline-end-width', '0'), - ]), - ] - : [ - atRoot([property('--tw-divide-x-reverse', '0')]), - - styleRule(':where(& > :not(:last-child))', [ - decl('--tw-sort', 'divide-x-width'), - borderProperties(), - decl('--tw-divide-x-reverse', '0'), - decl('border-inline-style', 'var(--tw-border-style)'), - decl('border-inline-start-width', `calc(${value} * var(--tw-divide-x-reverse))`), - decl( - 'border-inline-end-width', - `calc(${value} * calc(1 - var(--tw-divide-x-reverse)))`, - ), - ]), - ], + handle: (value) => [ + atRoot([property('--tw-divide-x-reverse', '0')]), + + styleRule(':where(& > :not(:last-child))', [ + decl('--tw-sort', 'divide-x-width'), + borderProperties(), + decl('--tw-divide-x-reverse', '0'), + decl('border-inline-style', 'var(--tw-border-style)'), + decl('border-inline-start-width', `calc(${value} * var(--tw-divide-x-reverse))`), + decl('border-inline-end-width', `calc(${value} * calc(1 - var(--tw-divide-x-reverse)))`), + ]), + ], }) functionalUtility('divide-y', { @@ -2607,36 +2570,19 @@ export function createUtilities(theme: Theme) { if (!isPositiveInteger(value)) return null return `${value}px` }, - handle: (value) => - // A zero border width resolves to zero regardless of the reverse - // direction, so we can skip the reverse variable entirely. - value === '0' || value === '0px' - ? [ - styleRule(':where(& > :not(:last-child))', [ - decl('--tw-sort', 'divide-y-width'), - borderProperties(), - decl('border-bottom-style', 'var(--tw-border-style)'), - decl('border-top-style', 'var(--tw-border-style)'), - decl('border-top-width', '0'), - decl('border-bottom-width', '0'), - ]), - ] - : [ - atRoot([property('--tw-divide-y-reverse', '0')]), - - styleRule(':where(& > :not(:last-child))', [ - decl('--tw-sort', 'divide-y-width'), - borderProperties(), - decl('--tw-divide-y-reverse', '0'), - decl('border-bottom-style', 'var(--tw-border-style)'), - decl('border-top-style', 'var(--tw-border-style)'), - decl('border-top-width', `calc(${value} * var(--tw-divide-y-reverse))`), - decl( - 'border-bottom-width', - `calc(${value} * calc(1 - var(--tw-divide-y-reverse)))`, - ), - ]), - ], + handle: (value) => [ + atRoot([property('--tw-divide-y-reverse', '0')]), + + styleRule(':where(& > :not(:last-child))', [ + decl('--tw-sort', 'divide-y-width'), + borderProperties(), + decl('--tw-divide-y-reverse', '0'), + decl('border-bottom-style', 'var(--tw-border-style)'), + decl('border-top-style', 'var(--tw-border-style)'), + decl('border-top-width', `calc(${value} * var(--tw-divide-y-reverse))`), + decl('border-bottom-width', `calc(${value} * calc(1 - var(--tw-divide-y-reverse)))`), + ]), + ], }) suggest('divide-x', () => [ @@ -3286,23 +3232,21 @@ export function createUtilities(theme: Theme) { { if (candidate.modifier) return - let value = candidate.value.value - let type = inferDataType(value, ['number', 'percentage']) + let type = inferDataType(candidate.value.value, ['number', 'percentage']) if (!type) return switch (type) { case 'number': { let multiplier = theme.resolve(null, ['--spacing']) if (!multiplier) return - if (!isValidSpacingMultiplier(value)) return + if (!isValidSpacingMultiplier(candidate.value.value)) return - if (Number(value) === 0) return desc.position('0') - return desc.position(`calc(${multiplier} * ${value})`) + return desc.position(`calc(${multiplier} * ${candidate.value.value})`) } case 'percentage': { - if (!isPositiveInteger(value.slice(0, -1))) return - return desc.position(value) + if (!isPositiveInteger(candidate.value.value.slice(0, -1))) return + return desc.position(candidate.value.value) } default: { @@ -5287,10 +5231,7 @@ export function createUtilities(theme: Theme) { if (!modifier && isValidSpacingMultiplier(candidate.modifier.value)) { let multiplier = theme.resolve(null, ['--spacing']) if (!multiplier) return null - modifier = - Number(candidate.modifier.value) === 0 - ? '0' - : `calc(${multiplier} * ${candidate.modifier.value})` + modifier = `calc(${multiplier} * ${candidate.modifier.value})` } // Shorthand for `leading-none` @@ -5343,10 +5284,7 @@ export function createUtilities(theme: Theme) { if (!modifier && isValidSpacingMultiplier(candidate.modifier.value)) { let multiplier = theme.resolve(null, ['--spacing']) if (!multiplier) return null - modifier = - Number(candidate.modifier.value) === 0 - ? '0' - : `calc(${multiplier} * ${candidate.modifier.value})` + modifier = `calc(${multiplier} * ${candidate.modifier.value})` } // Shorthand for `leading-none` From def1ac6daf6877ec92dbf8a8067080e70b0a1436 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 3 Jun 2026 15:28:00 +0200 Subject: [PATCH 03/10] =?UTF-8?q?optimize=20`0`=20values=20in=20`--spacing?= =?UTF-8?q?(=E2=80=A6)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tailwindcss/src/css-functions.test.ts | 46 ++++ packages/tailwindcss/src/css-functions.ts | 13 + packages/tailwindcss/src/utilities.test.ts | 223 +++++++++--------- packages/tailwindcss/src/utilities.ts | 10 +- 4 files changed, 179 insertions(+), 113 deletions(-) diff --git a/packages/tailwindcss/src/css-functions.test.ts b/packages/tailwindcss/src/css-functions.test.ts index 7096c6bbc75b..71357f98f18e 100644 --- a/packages/tailwindcss/src/css-functions.test.ts +++ b/packages/tailwindcss/src/css-functions.test.ts @@ -105,6 +105,52 @@ describe('--spacing(…)', () => { `) }) + describe('optimizations', () => { + test('--spacing(…) optimizes the output when the input is `0`', async () => { + expect( + await compileCss(css` + @theme { + --spacing: 0.25rem; + } + + .foo { + margin: --spacing(0); + padding: --spacing(0px); + } + `), + ).toMatchInlineSnapshot(` + " + .foo { + margin: 0; + padding: 0; + } + " + `) + }) + + test('--spacing(…) optimizes the output when the input is `0` (with an inline themed)', async () => { + expect( + await compileCss(css` + @theme inline { + --spacing: 0.25rem; + } + + .foo { + margin: --spacing(0); + padding: --spacing(0px); + } + `), + ).toMatchInlineSnapshot(` + " + .foo { + margin: 0; + padding: 0; + } + " + `) + }) + }) + test('--spacing(…) relies on `--spacing` to be defined', async () => { await expect(() => compileCss(css` diff --git a/packages/tailwindcss/src/css-functions.ts b/packages/tailwindcss/src/css-functions.ts index bb3e305e4b0f..7a290355ce46 100644 --- a/packages/tailwindcss/src/css-functions.ts +++ b/packages/tailwindcss/src/css-functions.ts @@ -2,6 +2,7 @@ import { Features } from '.' import { type AstNode } from './ast' import type { DesignSystem } from './design-system' import { withAlpha } from './utilities' +import { dimensions } from './utils/dimensions' import { segment } from './utils/segment' import * as ValueParser from './value-parser' import { walk, WalkAction } from './walk' @@ -62,6 +63,18 @@ function spacing( ) } + // Optimization: + // + // - We know that at this point the `--spacing` value must be set. + // - We know that `--spacing` must be set to a `` unit, such as `0.25rem` + // - We can assume that the `--spacing` value is not set to a `0`-like value. + // Otherwise `p-` would calculate as `0` which wouldn't make sense. + // + // That means that a value of `0` can be replaced by `0` + let valueDimension = dimensions.get(value) + if (valueDimension && valueDimension[0] === 0) return '0' + + // No known optimizations available, use full calculation return `calc(${multiplier} * ${value})` } diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index dcf6fb88d49c..2a1582646699 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -10026,6 +10026,12 @@ test('space-x', async () => { margin-inline-end: calc(calc(var(--spacing-4) * -1) * calc(1 - var(--tw-space-x-reverse))); } + :where(.space-x-0 > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(0 * var(--tw-space-x-reverse)); + margin-inline-end: calc(0 * calc(1 - var(--tw-space-x-reverse))); + } + :where(.space-x-4 > :not(:last-child)) { --tw-space-x-reverse: 0; margin-inline-start: calc(var(--spacing-4) * var(--tw-space-x-reverse)); @@ -10038,10 +10044,6 @@ test('space-x', async () => { margin-inline-end: calc(4px * calc(1 - var(--tw-space-x-reverse))); } - :where(.space-x-0 > :not(:last-child)) { - margin-inline: 0; - } - @property --tw-space-x-reverse { syntax: "*"; inherits: false; @@ -10084,6 +10086,12 @@ test('space-y', async () => { margin-block-end: calc(calc(var(--spacing-4) * -1) * calc(1 - var(--tw-space-y-reverse))); } + :where(.space-y-0 > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(0 * var(--tw-space-y-reverse)); + margin-block-end: calc(0 * calc(1 - var(--tw-space-y-reverse))); + } + :where(.space-y-4 > :not(:last-child)) { --tw-space-y-reverse: 0; margin-block-start: calc(var(--spacing-4) * var(--tw-space-y-reverse)); @@ -10096,10 +10104,6 @@ test('space-y', async () => { margin-block-end: calc(4px * calc(1 - var(--tw-space-y-reverse))); } - :where(.space-y-0 > :not(:last-child)) { - margin-block: 0; - } - @property --tw-space-y-reverse { syntax: "*"; inherits: false; @@ -10163,62 +10167,64 @@ test('space-y-reverse', async () => { test('divide-x', async () => { expect(await run(['divide-x', 'divide-x-0', 'divide-x-4', 'divide-x-123', 'divide-x-[4px]'])) .toMatchInlineSnapshot(` - " - @layer properties { - @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { - *, :before, :after, ::backdrop { - --tw-divide-x-reverse: 0; - --tw-border-style: solid; + " + @layer properties { + @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { + *, :before, :after, ::backdrop { + --tw-divide-x-reverse: 0; + --tw-border-style: solid; + } } } - } - :where(.divide-x > :not(:last-child)) { - --tw-divide-x-reverse: 0; - border-inline-style: var(--tw-border-style); - border-inline-start-width: calc(1px * var(--tw-divide-x-reverse)); - border-inline-end-width: calc(1px * calc(1 - var(--tw-divide-x-reverse))); - } + :where(.divide-x > :not(:last-child)) { + --tw-divide-x-reverse: 0; + border-inline-style: var(--tw-border-style); + border-inline-start-width: calc(1px * var(--tw-divide-x-reverse)); + border-inline-end-width: calc(1px * calc(1 - var(--tw-divide-x-reverse))); + } - :where(.divide-x-4 > :not(:last-child)) { - --tw-divide-x-reverse: 0; - border-inline-style: var(--tw-border-style); - border-inline-start-width: calc(4px * var(--tw-divide-x-reverse)); - border-inline-end-width: calc(4px * calc(1 - var(--tw-divide-x-reverse))); - } + :where(.divide-x-0 > :not(:last-child)) { + --tw-divide-x-reverse: 0; + border-inline-style: var(--tw-border-style); + border-inline-start-width: calc(0px * var(--tw-divide-x-reverse)); + border-inline-end-width: calc(0px * calc(1 - var(--tw-divide-x-reverse))); + } - :where(.divide-x-123 > :not(:last-child)) { - --tw-divide-x-reverse: 0; - border-inline-style: var(--tw-border-style); - border-inline-start-width: calc(123px * var(--tw-divide-x-reverse)); - border-inline-end-width: calc(123px * calc(1 - var(--tw-divide-x-reverse))); - } + :where(.divide-x-4 > :not(:last-child)) { + --tw-divide-x-reverse: 0; + border-inline-style: var(--tw-border-style); + border-inline-start-width: calc(4px * var(--tw-divide-x-reverse)); + border-inline-end-width: calc(4px * calc(1 - var(--tw-divide-x-reverse))); + } - :where(.divide-x-\\[4px\\] > :not(:last-child)) { - --tw-divide-x-reverse: 0; - border-inline-style: var(--tw-border-style); - border-inline-start-width: calc(4px * var(--tw-divide-x-reverse)); - border-inline-end-width: calc(4px * calc(1 - var(--tw-divide-x-reverse))); - } + :where(.divide-x-123 > :not(:last-child)) { + --tw-divide-x-reverse: 0; + border-inline-style: var(--tw-border-style); + border-inline-start-width: calc(123px * var(--tw-divide-x-reverse)); + border-inline-end-width: calc(123px * calc(1 - var(--tw-divide-x-reverse))); + } - :where(.divide-x-0 > :not(:last-child)) { - border-inline-style: var(--tw-border-style); - border-inline-width: 0; - } + :where(.divide-x-\\[4px\\] > :not(:last-child)) { + --tw-divide-x-reverse: 0; + border-inline-style: var(--tw-border-style); + border-inline-start-width: calc(4px * var(--tw-divide-x-reverse)); + border-inline-end-width: calc(4px * calc(1 - var(--tw-divide-x-reverse))); + } - @property --tw-divide-x-reverse { - syntax: "*"; - inherits: false; - initial-value: 0; - } + @property --tw-divide-x-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; + } - @property --tw-border-style { - syntax: "*"; - inherits: false; - initial-value: solid; - } - " - `) + @property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; + } + " + `) expect( await run([ '-divide-x', @@ -10282,68 +10288,69 @@ test('divide-x with custom default border width', async () => { test('divide-y', async () => { expect(await run(['divide-y', 'divide-y-0', 'divide-y-4', 'divide-y-123', 'divide-y-[4px]'])) .toMatchInlineSnapshot(` - " - @layer properties { - @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { - *, :before, :after, ::backdrop { - --tw-divide-y-reverse: 0; - --tw-border-style: solid; + " + @layer properties { + @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { + *, :before, :after, ::backdrop { + --tw-divide-y-reverse: 0; + --tw-border-style: solid; + } } } - } - :where(.divide-y > :not(:last-child)) { - --tw-divide-y-reverse: 0; - border-bottom-style: var(--tw-border-style); - border-top-style: var(--tw-border-style); - border-top-width: calc(1px * var(--tw-divide-y-reverse)); - border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); - } + :where(.divide-y > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(1px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + } - :where(.divide-y-4 > :not(:last-child)) { - --tw-divide-y-reverse: 0; - border-bottom-style: var(--tw-border-style); - border-top-style: var(--tw-border-style); - border-top-width: calc(4px * var(--tw-divide-y-reverse)); - border-bottom-width: calc(4px * calc(1 - var(--tw-divide-y-reverse))); - } + :where(.divide-y-0 > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(0px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(0px * calc(1 - var(--tw-divide-y-reverse))); + } - :where(.divide-y-123 > :not(:last-child)) { - --tw-divide-y-reverse: 0; - border-bottom-style: var(--tw-border-style); - border-top-style: var(--tw-border-style); - border-top-width: calc(123px * var(--tw-divide-y-reverse)); - border-bottom-width: calc(123px * calc(1 - var(--tw-divide-y-reverse))); - } + :where(.divide-y-4 > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(4px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(4px * calc(1 - var(--tw-divide-y-reverse))); + } - :where(.divide-y-\\[4px\\] > :not(:last-child)) { - --tw-divide-y-reverse: 0; - border-bottom-style: var(--tw-border-style); - border-top-style: var(--tw-border-style); - border-top-width: calc(4px * var(--tw-divide-y-reverse)); - border-bottom-width: calc(4px * calc(1 - var(--tw-divide-y-reverse))); - } + :where(.divide-y-123 > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(123px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(123px * calc(1 - var(--tw-divide-y-reverse))); + } - :where(.divide-y-0 > :not(:last-child)) { - border-bottom-style: var(--tw-border-style); - border-top-style: var(--tw-border-style); - border-top-width: 0; - border-bottom-width: 0; - } + :where(.divide-y-\\[4px\\] > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(4px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(4px * calc(1 - var(--tw-divide-y-reverse))); + } - @property --tw-divide-y-reverse { - syntax: "*"; - inherits: false; - initial-value: 0; - } + @property --tw-divide-y-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; + } - @property --tw-border-style { - syntax: "*"; - inherits: false; - initial-value: solid; - } - " - `) + @property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; + } + " + `) expect( await run([ '-divide-y', diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index b964ecc9310a..a5b05dcdb878 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -550,14 +550,14 @@ export function createUtilities(theme: Theme) { if (!multiplier) return null if (!isValidSpacingMultiplier(value)) return null - return `calc(${multiplier} * ${value})` + return `--spacing(${value})` }, handleNegativeBareValue: ({ value }) => { let multiplier = theme.resolve(null, ['--spacing']) if (!multiplier) return null if (!isValidSpacingMultiplier(value)) return null - return `calc(${multiplier} * -${value})` + return `--spacing(-${value})` }, handle, staticValues, @@ -3241,7 +3241,7 @@ export function createUtilities(theme: Theme) { if (!multiplier) return if (!isValidSpacingMultiplier(candidate.value.value)) return - return desc.position(`calc(${multiplier} * ${candidate.value.value})`) + return desc.position(`--spacing(${candidate.value.value})`) } case 'percentage': { @@ -5231,7 +5231,7 @@ export function createUtilities(theme: Theme) { if (!modifier && isValidSpacingMultiplier(candidate.modifier.value)) { let multiplier = theme.resolve(null, ['--spacing']) if (!multiplier) return null - modifier = `calc(${multiplier} * ${candidate.modifier.value})` + modifier = `--spacing(${candidate.modifier.value})` } // Shorthand for `leading-none` @@ -5284,7 +5284,7 @@ export function createUtilities(theme: Theme) { if (!modifier && isValidSpacingMultiplier(candidate.modifier.value)) { let multiplier = theme.resolve(null, ['--spacing']) if (!multiplier) return null - modifier = `calc(${multiplier} * ${candidate.modifier.value})` + modifier = `--spacing(${candidate.modifier.value})` } // Shorthand for `leading-none` From 330e6fed2de7c186469be353f449544e5951e19a Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 3 Jun 2026 16:28:32 +0200 Subject: [PATCH 04/10] =?UTF-8?q?optimize=20`1`=20values=20in=20`--spacing?= =?UTF-8?q?(=E2=80=A6)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tailwindcss/src/css-functions.test.ts | 48 ++++++++ packages/tailwindcss/src/css-functions.ts | 8 +- packages/tailwindcss/src/utilities.test.ts | 110 ++++++++++++++---- packages/tailwindcss/src/utilities.ts | 16 ++- 4 files changed, 151 insertions(+), 31 deletions(-) diff --git a/packages/tailwindcss/src/css-functions.test.ts b/packages/tailwindcss/src/css-functions.test.ts index 71357f98f18e..3c26a05384df 100644 --- a/packages/tailwindcss/src/css-functions.test.ts +++ b/packages/tailwindcss/src/css-functions.test.ts @@ -149,6 +149,54 @@ describe('--spacing(…)', () => { " `) }) + + test('--spacing(…) optimizes the output when the input is `1`', async () => { + expect( + await compileCss(css` + @theme { + --spacing: 0.25rem; + } + + .foo { + margin: --spacing(1); + padding: --spacing(1px); + } + `), + ).toMatchInlineSnapshot(` + " + :root, :host { + --spacing: .25rem; + } + + .foo { + margin: var(--spacing); + padding: var(--spacing); + } + " + `) + }) + + test('--spacing(…) optimizes the output when the input is `1` (with an inline themed)', async () => { + expect( + await compileCss(css` + @theme inline { + --spacing: 0.25rem; + } + + .foo { + margin: --spacing(1); + padding: --spacing(1px); + } + `), + ).toMatchInlineSnapshot(` + " + .foo { + margin: .25rem; + padding: .25rem; + } + " + `) + }) }) test('--spacing(…) relies on `--spacing` to be defined', async () => { diff --git a/packages/tailwindcss/src/css-functions.ts b/packages/tailwindcss/src/css-functions.ts index 7a290355ce46..66b8b6728a0e 100644 --- a/packages/tailwindcss/src/css-functions.ts +++ b/packages/tailwindcss/src/css-functions.ts @@ -70,9 +70,13 @@ function spacing( // - We can assume that the `--spacing` value is not set to a `0`-like value. // Otherwise `p-` 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 `0` + // - That means that a value of `1` can be replaced by `multiplier` let valueDimension = dimensions.get(value) - if (valueDimension && valueDimension[0] === 0) return '0' + if (valueDimension) { + if (valueDimension[0] === 0) return '0' + if (valueDimension[0] === 1) return multiplier + } // No known optimizations available, use full calculation return `calc(${multiplier} * ${value})` diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 2a1582646699..9c78fa3ee3eb 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -2170,7 +2170,7 @@ test('mx', async () => { } .mx-1 { - margin-inline: calc(var(--spacing) * 1); + margin-inline: var(--spacing); } .mx-4 { @@ -2262,7 +2262,7 @@ test('my', async () => { } .my-1 { - margin-block: calc(var(--spacing) * 1); + margin-block: var(--spacing); } .my-2\\.5 { @@ -2350,7 +2350,7 @@ test('mt', async () => { } .mt-1 { - margin-top: calc(var(--spacing) * 1); + margin-top: var(--spacing); } .mt-2\\.5 { @@ -2438,7 +2438,7 @@ test('ms', async () => { } .ms-1 { - margin-inline-start: calc(var(--spacing) * 1); + margin-inline-start: var(--spacing); } .ms-2\\.5 { @@ -2526,7 +2526,7 @@ test('me', async () => { } .me-1 { - margin-inline-end: calc(var(--spacing) * 1); + margin-inline-end: var(--spacing); } .me-2\\.5 { @@ -2614,7 +2614,7 @@ test('mbs', async () => { } .mbs-1 { - margin-block-start: calc(var(--spacing) * 1); + margin-block-start: var(--spacing); } .mbs-2\\.5 { @@ -2702,7 +2702,7 @@ test('mbe', async () => { } .mbe-1 { - margin-block-end: calc(var(--spacing) * 1); + margin-block-end: var(--spacing); } .mbe-2\\.5 { @@ -2790,7 +2790,7 @@ test('mr', async () => { } .mr-1 { - margin-right: calc(var(--spacing) * 1); + margin-right: var(--spacing); } .mr-2\\.5 { @@ -2878,7 +2878,7 @@ test('mb', async () => { } .mb-1 { - margin-bottom: calc(var(--spacing) * 1); + margin-bottom: var(--spacing); } .mb-2\\.5 { @@ -2966,7 +2966,7 @@ test('ml', async () => { } .ml-1 { - margin-left: calc(var(--spacing) * 1); + margin-left: var(--spacing); } .ml-2\\.5 { @@ -19355,8 +19355,16 @@ test('mask-y-to', async () => { }) test('mask-linear', async () => { - expect(await run(['mask-linear-0', 'mask-linear-45', 'mask-linear-[3rad]', '-mask-linear-45'])) - .toMatchInlineSnapshot(` + expect( + await run([ + 'mask-linear-0', + 'mask-linear-1', + '-mask-linear-1', + 'mask-linear-45', + 'mask-linear-[3rad]', + '-mask-linear-45', + ]), + ).toMatchInlineSnapshot(` " @layer properties { @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { @@ -19373,6 +19381,17 @@ test('mask-linear', async () => { } } + .-mask-linear-1 { + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + --tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops, var(--tw-mask-linear-position))); + --tw-mask-linear-position: -1deg; + -webkit-mask-composite: source-in; + -webkit-mask-composite: source-in; + mask-composite: intersect; + } + .-mask-linear-45 { -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); @@ -19395,6 +19414,17 @@ test('mask-linear', async () => { mask-composite: intersect; } + .mask-linear-1 { + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + --tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops, var(--tw-mask-linear-position))); + --tw-mask-linear-position: 1deg; + -webkit-mask-composite: source-in; + -webkit-mask-composite: source-in; + mask-composite: intersect; + } + .mask-linear-45 { -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); @@ -20721,8 +20751,16 @@ test('mask-radial-to', async () => { }) test('mask-conic', async () => { - expect(await run(['mask-conic-0', 'mask-conic-45', 'mask-conic-[3rad]', '-mask-conic-45'])) - .toMatchInlineSnapshot(` + expect( + await run([ + 'mask-conic-0', + 'mask-conic-1', + '-mask-conic-1', + 'mask-conic-45', + 'mask-conic-[3rad]', + '-mask-conic-45', + ]), + ).toMatchInlineSnapshot(` " @layer properties { @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { @@ -20739,6 +20777,17 @@ test('mask-conic', async () => { } } + .-mask-conic-1 { + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + --tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops, var(--tw-mask-conic-position))); + --tw-mask-conic-position: -1deg; + -webkit-mask-composite: source-in; + -webkit-mask-composite: source-in; + mask-composite: intersect; + } + .-mask-conic-45 { -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); @@ -20761,6 +20810,17 @@ test('mask-conic', async () => { mask-composite: intersect; } + .mask-conic-1 { + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + --tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops, var(--tw-mask-conic-position))); + --tw-mask-conic-position: 1deg; + -webkit-mask-composite: source-in; + -webkit-mask-composite: source-in; + mask-composite: intersect; + } + .mask-conic-45 { -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); @@ -22472,7 +22532,7 @@ test('p', async () => { } .p-1 { - padding: calc(var(--spacing) * 1); + padding: var(--spacing); } .p-4 { @@ -22515,7 +22575,7 @@ test('px', async () => { } .px-1 { - padding-inline: calc(var(--spacing) * 1); + padding-inline: var(--spacing); } .px-2\\.5 { @@ -22558,7 +22618,7 @@ test('py', async () => { } .py-1 { - padding-block: calc(var(--spacing) * 1); + padding-block: var(--spacing); } .py-4 { @@ -22601,7 +22661,7 @@ test('pt', async () => { } .pt-1 { - padding-top: calc(var(--spacing) * 1); + padding-top: var(--spacing); } .pt-4 { @@ -22644,7 +22704,7 @@ test('ps', async () => { } .ps-1 { - padding-inline-start: calc(var(--spacing) * 1); + padding-inline-start: var(--spacing); } .ps-4 { @@ -22687,7 +22747,7 @@ test('pe', async () => { } .pe-1 { - padding-inline-end: calc(var(--spacing) * 1); + padding-inline-end: var(--spacing); } .pe-4 { @@ -22730,7 +22790,7 @@ test('pbs', async () => { } .pbs-1 { - padding-block-start: calc(var(--spacing) * 1); + padding-block-start: var(--spacing); } .pbs-4 { @@ -22773,7 +22833,7 @@ test('pbe', async () => { } .pbe-1 { - padding-block-end: calc(var(--spacing) * 1); + padding-block-end: var(--spacing); } .pbe-4 { @@ -22816,7 +22876,7 @@ test('pr', async () => { } .pr-1 { - padding-right: calc(var(--spacing) * 1); + padding-right: var(--spacing); } .pr-4 { @@ -22859,7 +22919,7 @@ test('pb', async () => { } .pb-1 { - padding-bottom: calc(var(--spacing) * 1); + padding-bottom: var(--spacing); } .pb-4 { @@ -22902,7 +22962,7 @@ test('pl', async () => { } .pl-1 { - padding-left: calc(var(--spacing) * 1); + padding-left: var(--spacing); } .pl-4 { diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index a5b05dcdb878..96786921d4b4 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -3418,12 +3418,16 @@ export function createUtilities(theme: Theme) { supportsFractions: false, handleBareValue({ value }) { if (!isPositiveInteger(value)) return null - if (Number(value) === 0) return '0deg' + let valueAsNumber = Number(value) + if (valueAsNumber === 0) return '0deg' + if (valueAsNumber === 1) return '1deg' return `calc(1deg * ${value})` }, handleNegativeBareValue({ value }) { if (!isPositiveInteger(value)) return null - if (Number(value) === 0) return '0deg' + let valueAsNumber = Number(value) + if (valueAsNumber === 0) return '0deg' + if (valueAsNumber === 1) return '-1deg' return `calc(1deg * -${value})` }, handle: (value) => [ @@ -3638,12 +3642,16 @@ export function createUtilities(theme: Theme) { supportsFractions: false, handleBareValue({ value }) { if (!isPositiveInteger(value)) return null - if (Number(value) === 0) return '0deg' + let valueAsNumber = Number(value) + if (valueAsNumber === 0) return '0deg' + if (valueAsNumber === 1) return '1deg' return `calc(1deg * ${value})` }, handleNegativeBareValue({ value }) { if (!isPositiveInteger(value)) return null - if (Number(value) === 0) return '0deg' + let valueAsNumber = Number(value) + if (valueAsNumber === 0) return '0deg' + if (valueAsNumber === 1) return '-1deg' return `calc(1deg * -${value})` }, handle: (value) => [ From 6ca3cfbff6930d512d429409f3de30ff22b3fc51 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 3 Jun 2026 16:39:07 +0200 Subject: [PATCH 05/10] update changelog --- CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c31e587d0113..1ec07535b622 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `--silent` option to suppress output in `@tailwindcss/cli` ([#20100](https://github.com/tailwindlabs/tailwindcss/pull/20100)) -### Changed - -- Skip redundant `calc(…)` for utilities whose computed value is zero (e.g. `m-0`, `px-0`, `left-0`, `space-x-0`, `divide-x-0`, `text-sm/0`, `mask-linear-0`) - ### Fixed - Remove deprecation warnings by using `Module#registerHooks` instead of `Module#register` on Node 26+ ([#20028](https://github.com/tailwindlabs/tailwindcss/pull/20028)) @@ -29,6 +25,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure `@tailwindcss/cli` in `--watch` mode recovers when a tracked dependency is deleted and restored ([#20137](https://github.com/tailwindlabs/tailwindcss/pull/20137)) - Ensure standalone `@tailwindcss/cli` binaries are ignored when scanning for class candidates ([#20139](https://github.com/tailwindlabs/tailwindcss/pull/20139)) +### Changed + +- Generate `0` instead of `calc(var(--spacing) * 0)` for spacing utilities like `m-0` and `left-0` ([#20196](https://github.com/tailwindlabs/tailwindcss/pull/20196)) +- Generate `var(--spacing)` instead of `calc(var(--spacing) * 1)` for spacing utilities like `m-1` and `left-1` ([#20196](https://github.com/tailwindlabs/tailwindcss/pull/20196)) + ## [4.3.0] - 2026-05-08 ### Added From 70854a540f9db2aa4bdf96b326e64ea52322fce2 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 3 Jun 2026 17:04:44 +0200 Subject: [PATCH 06/10] remove redundant test --- packages/tailwindcss/src/utilities.test.ts | 75 ---------------------- 1 file changed, 75 deletions(-) diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 9c78fa3ee3eb..bda672d5f88b 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -318,81 +318,6 @@ test('inset', async () => { ).toEqual('') }) -test('inset/position utilities with a `0` value emit a constant, not calc()', async () => { - let output = await run( - [ - 'inset-0', - 'inset-x-0', - 'inset-y-0', - 'inset-s-0', - 'inset-e-0', - 'inset-bs-0', - 'inset-be-0', - 'top-0', - 'right-0', - 'bottom-0', - 'left-0', - // Negative zero must collapse too (no `calc(0 * -1)`). - '-inset-0', - '-top-0', - ], - css` - @theme { - --spacing: 0.25rem; - } - @tailwind utilities; - `, - ) - - expect(output).toMatchInlineSnapshot(` - " - .-inset-0, .inset-0 { - inset: 0; - } - - .inset-x-0 { - inset-inline: 0; - } - - .inset-y-0 { - inset-block: 0; - } - - .inset-s-0 { - inset-inline-start: 0; - } - - .inset-e-0 { - inset-inline-end: 0; - } - - .inset-bs-0 { - inset-block-start: 0; - } - - .inset-be-0 { - inset-block-end: 0; - } - - .-top-0, .top-0 { - top: 0; - } - - .right-0 { - right: 0; - } - - .bottom-0 { - bottom: 0; - } - - .left-0 { - left: 0; - } - " - `) -}) - test('inset-x', async () => { expect( await run( From 787006f09fc1030cd3cc31132941d19e6f344d28 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 3 Jun 2026 18:20:41 +0200 Subject: [PATCH 07/10] handle `0` in `space-x-0` and `space-y-0` utilities --- packages/tailwindcss/src/utilities.test.ts | 64 +++++++++++++++++++-- packages/tailwindcss/src/utilities.ts | 65 ++++++++++++++++------ 2 files changed, 105 insertions(+), 24 deletions(-) diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index bda672d5f88b..f461d25477e5 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -9922,7 +9922,16 @@ test('gap-y', async () => { test('space-x', async () => { expect( await run( - ['space-x-0', 'space-x-4', 'space-x-[4px]', '-space-x-4'], + [ + 'space-x-0', + 'space-x-[0]', + 'space-x-[0px]', + 'space-x-1', + '-space-x-1', + 'space-x-4', + 'space-x-[4px]', + '-space-x-4', + ], css` @theme { --spacing: 0.25rem; @@ -9942,9 +9951,16 @@ test('space-x', async () => { } :root, :host { + --spacing: .25rem; --spacing-4: 1rem; } + :where(.-space-x-1 > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * -1) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * -1) * calc(1 - var(--tw-space-x-reverse))); + } + :where(.-space-x-4 > :not(:last-child)) { --tw-space-x-reverse: 0; margin-inline-start: calc(calc(var(--spacing-4) * -1) * var(--tw-space-x-reverse)); @@ -9953,8 +9969,13 @@ test('space-x', async () => { :where(.space-x-0 > :not(:last-child)) { --tw-space-x-reverse: 0; - margin-inline-start: calc(0 * var(--tw-space-x-reverse)); - margin-inline-end: calc(0 * calc(1 - var(--tw-space-x-reverse))); + margin-inline: 0; + } + + :where(.space-x-1 > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(var(--spacing) * var(--tw-space-x-reverse)); + margin-inline-end: calc(var(--spacing) * calc(1 - var(--tw-space-x-reverse))); } :where(.space-x-4 > :not(:last-child)) { @@ -9963,6 +9984,11 @@ test('space-x', async () => { margin-inline-end: calc(var(--spacing-4) * calc(1 - var(--tw-space-x-reverse))); } + :where(.space-x-\\[0\\] > :not(:last-child)), :where(.space-x-\\[0px\\] > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline: 0; + } + :where(.space-x-\\[4px\\] > :not(:last-child)) { --tw-space-x-reverse: 0; margin-inline-start: calc(4px * var(--tw-space-x-reverse)); @@ -9982,7 +10008,16 @@ test('space-x', async () => { test('space-y', async () => { expect( await run( - ['space-y-0', 'space-y-4', 'space-y-[4px]', '-space-y-4'], + [ + 'space-y-0', + 'space-y-[0]', + 'space-y-[0px]', + 'space-y-1', + '-space-y-1', + 'space-y-4', + 'space-y-[4px]', + '-space-y-4', + ], css` @theme { --spacing: 0.25rem; @@ -10002,9 +10037,16 @@ test('space-y', async () => { } :root, :host { + --spacing: .25rem; --spacing-4: 1rem; } + :where(.-space-y-1 > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * -1) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * -1) * calc(1 - var(--tw-space-y-reverse))); + } + :where(.-space-y-4 > :not(:last-child)) { --tw-space-y-reverse: 0; margin-block-start: calc(calc(var(--spacing-4) * -1) * var(--tw-space-y-reverse)); @@ -10013,8 +10055,13 @@ test('space-y', async () => { :where(.space-y-0 > :not(:last-child)) { --tw-space-y-reverse: 0; - margin-block-start: calc(0 * var(--tw-space-y-reverse)); - margin-block-end: calc(0 * calc(1 - var(--tw-space-y-reverse))); + margin-block: 0; + } + + :where(.space-y-1 > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(var(--spacing) * var(--tw-space-y-reverse)); + margin-block-end: calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse))); } :where(.space-y-4 > :not(:last-child)) { @@ -10023,6 +10070,11 @@ test('space-y', async () => { margin-block-end: calc(var(--spacing-4) * calc(1 - var(--tw-space-y-reverse))); } + :where(.space-y-\\[0\\] > :not(:last-child)), :where(.space-y-\\[0px\\] > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block: 0; + } + :where(.space-y-\\[4px\\] > :not(:last-child)) { --tw-space-y-reverse: 0; margin-block-start: calc(4px * var(--tw-space-y-reverse)); diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 96786921d4b4..427162b690fa 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -15,6 +15,7 @@ import type { DesignSystem } from './design-system' import type { Theme, ThemeKey } from './theme' import { compareBreakpoints } from './utils/compare-breakpoints' import { DefaultMap } from './utils/default-map' +import { dimensions } from './utils/dimensions' import { unescape } from './utils/escape' import { inferDataType, @@ -2119,31 +2120,59 @@ export function createUtilities(theme: Theme) { spacingUtility( 'space-x', ['--space', '--spacing'], - (value) => [ - atRoot([property('--tw-space-x-reverse', '0')]), + (value) => { + let zero = (() => { + if (value === '--spacing(0)') return true - styleRule(':where(& > :not(:last-child))', [ - decl('--tw-sort', 'row-gap'), - decl('--tw-space-x-reverse', '0'), - decl('margin-inline-start', `calc(${value} * var(--tw-space-x-reverse))`), - decl('margin-inline-end', `calc(${value} * calc(1 - var(--tw-space-x-reverse)))`), - ]), - ], + let parsed = dimensions.get(value) + if (parsed && parsed[0] === 0) return true + + return false + })() + + return [ + atRoot([property('--tw-space-x-reverse', '0')]), + + styleRule(':where(& > :not(:last-child))', [ + decl('--tw-sort', 'row-gap'), + decl('--tw-space-x-reverse', '0'), + decl('margin-inline-start', zero ? '0' : `calc(${value} * var(--tw-space-x-reverse))`), + decl( + 'margin-inline-end', + zero ? '0' : `calc(${value} * calc(1 - var(--tw-space-x-reverse)))`, + ), + ]), + ] + }, { supportsNegative: true }, ) spacingUtility( 'space-y', ['--space', '--spacing'], - (value) => [ - atRoot([property('--tw-space-y-reverse', '0')]), - styleRule(':where(& > :not(:last-child))', [ - decl('--tw-sort', 'column-gap'), - decl('--tw-space-y-reverse', '0'), - decl('margin-block-start', `calc(${value} * var(--tw-space-y-reverse))`), - decl('margin-block-end', `calc(${value} * calc(1 - var(--tw-space-y-reverse)))`), - ]), - ], + (value) => { + let zero = (() => { + if (value === '--spacing(0)') return true + + let parsed = dimensions.get(value) + if (parsed && parsed[0] === 0) return true + + return false + })() + + return [ + atRoot([property('--tw-space-y-reverse', '0')]), + styleRule(':where(& > :not(:last-child))', [ + decl('--tw-sort', 'column-gap'), + decl('--tw-space-y-reverse', '0'), + decl('margin-block-start', zero ? '0' : `calc(${value} * var(--tw-space-y-reverse))`), + decl( + 'margin-block-end', + zero ? '0' : `calc(${value} * calc(1 - var(--tw-space-y-reverse)))`, + ), + ]), + ] + }, { supportsNegative: true }, ) From 1ae070e5dbf7409386af4c763342509453c1abb1 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 3 Jun 2026 18:23:33 +0200 Subject: [PATCH 08/10] fix typo --- packages/tailwindcss/src/css-functions.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss/src/css-functions.test.ts b/packages/tailwindcss/src/css-functions.test.ts index 3c26a05384df..0e4146fdff69 100644 --- a/packages/tailwindcss/src/css-functions.test.ts +++ b/packages/tailwindcss/src/css-functions.test.ts @@ -128,7 +128,7 @@ describe('--spacing(…)', () => { `) }) - test('--spacing(…) optimizes the output when the input is `0` (with an inline themed)', async () => { + test('--spacing(…) optimizes the output when the input is `0` (with an inlined theme value)', async () => { expect( await compileCss(css` @theme inline { @@ -176,7 +176,7 @@ describe('--spacing(…)', () => { `) }) - test('--spacing(…) optimizes the output when the input is `1` (with an inline themed)', async () => { + test('--spacing(…) optimizes the output when the input is `1` (with an inlined theme value)', async () => { expect( await compileCss(css` @theme inline { From f1aeba3e8326d6c5615e2dc675e4abe818ecb5b1 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 3 Jun 2026 18:31:34 +0200 Subject: [PATCH 09/10] handle `-0` values for `space-x-*` and `space-y-*` --- packages/tailwindcss/src/utilities.test.ts | 20 ++++++++++++++++++-- packages/tailwindcss/src/utilities.ts | 2 ++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index f461d25477e5..25344dcf9121 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -9926,6 +9926,9 @@ test('space-x', async () => { 'space-x-0', 'space-x-[0]', 'space-x-[0px]', + '-space-x-0', + 'space-x-[-0]', + 'space-x-[-0px]', 'space-x-1', '-space-x-1', 'space-x-4', @@ -9955,6 +9958,11 @@ test('space-x', async () => { --spacing-4: 1rem; } + :where(.-space-x-0 > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline: 0; + } + :where(.-space-x-1 > :not(:last-child)) { --tw-space-x-reverse: 0; margin-inline-start: calc(calc(var(--spacing) * -1) * var(--tw-space-x-reverse)); @@ -9984,7 +9992,7 @@ test('space-x', async () => { margin-inline-end: calc(var(--spacing-4) * calc(1 - var(--tw-space-x-reverse))); } - :where(.space-x-\\[0\\] > :not(:last-child)), :where(.space-x-\\[0px\\] > :not(:last-child)) { + :where(.space-x-\\[-0\\] > :not(:last-child)), :where(.space-x-\\[-0px\\] > :not(:last-child)), :where(.space-x-\\[0\\] > :not(:last-child)), :where(.space-x-\\[0px\\] > :not(:last-child)) { --tw-space-x-reverse: 0; margin-inline: 0; } @@ -10012,6 +10020,9 @@ test('space-y', async () => { 'space-y-0', 'space-y-[0]', 'space-y-[0px]', + '-space-y-0', + 'space-y-[-0]', + 'space-y-[-0px]', 'space-y-1', '-space-y-1', 'space-y-4', @@ -10041,6 +10052,11 @@ test('space-y', async () => { --spacing-4: 1rem; } + :where(.-space-y-0 > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block: 0; + } + :where(.-space-y-1 > :not(:last-child)) { --tw-space-y-reverse: 0; margin-block-start: calc(calc(var(--spacing) * -1) * var(--tw-space-y-reverse)); @@ -10070,7 +10086,7 @@ test('space-y', async () => { margin-block-end: calc(var(--spacing-4) * calc(1 - var(--tw-space-y-reverse))); } - :where(.space-y-\\[0\\] > :not(:last-child)), :where(.space-y-\\[0px\\] > :not(:last-child)) { + :where(.space-y-\\[-0\\] > :not(:last-child)), :where(.space-y-\\[-0px\\] > :not(:last-child)), :where(.space-y-\\[0\\] > :not(:last-child)), :where(.space-y-\\[0px\\] > :not(:last-child)) { --tw-space-y-reverse: 0; margin-block: 0; } diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 427162b690fa..ed2a7f6bc832 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -2123,6 +2123,7 @@ export function createUtilities(theme: Theme) { (value) => { let zero = (() => { if (value === '--spacing(0)') return true + if (value === '--spacing(-0)') return true let parsed = dimensions.get(value) if (parsed && parsed[0] === 0) return true @@ -2153,6 +2154,7 @@ export function createUtilities(theme: Theme) { (value) => { let zero = (() => { if (value === '--spacing(0)') return true + if (value === '--spacing(-0)') return true let parsed = dimensions.get(value) if (parsed && parsed[0] === 0) return true From 4a9723c443992aa9d7530599b65ac4a0a718ff1b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 3 Jun 2026 18:38:22 +0200 Subject: [PATCH 10/10] keep invalid data invalid `space-x-[0deg]` stays as: ```css .space-x-\[0deg\] { :where(& > :not(:last-child)) { --tw-space-x-reverse: 0; margin-inline-start: calc(0deg * var(--tw-space-x-reverse)); margin-inline-end: calc(0deg * calc(1 - var(--tw-space-x-reverse))); } } ``` And will not be optimized to just: ```css .space-x-\[0deg\] { :where(& > :not(:last-child)) { --tw-space-x-reverse: 0; margin-inline: 0; } } ``` --- packages/tailwindcss/src/utilities.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index ed2a7f6bc832..2d9b4a3c6c82 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -19,6 +19,7 @@ import { dimensions } from './utils/dimensions' import { unescape } from './utils/escape' import { inferDataType, + isLength, isPositiveInteger, isStrictPositiveInteger, isValidOpacityValue, @@ -2126,7 +2127,9 @@ export function createUtilities(theme: Theme) { if (value === '--spacing(-0)') return true let parsed = dimensions.get(value) - if (parsed && parsed[0] === 0) return true + if (parsed && parsed[0] === 0 && (parsed[1] === null || isLength(value))) { + return true + } return false })() @@ -2157,7 +2160,9 @@ export function createUtilities(theme: Theme) { if (value === '--spacing(-0)') return true let parsed = dimensions.get(value) - if (parsed && parsed[0] === 0) return true + if (parsed && parsed[0] === 0 && (parsed[1] === null || isLength(value))) { + return true + } return false })()