diff --git a/CHANGELOG.md b/CHANGELOG.md index 64c0ea940f13..4de6bd1f2cad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure `@tailwindcss/oxide` falls back to WASM on platforms without native bindings ([#20383](https://github.com/tailwindlabs/tailwindcss/pull/20383)) - Detect classes in Ruby percent literals using angle brackets or custom delimiters (e.g. `%w`, `%w|flex|`), including in Slim and Haml templates ([#20387](https://github.com/tailwindlabs/tailwindcss/pull/20387)) - Preserve whitespace in `--default(…)` values in custom functional utilities (e.g. `--default(box alphabetic)` no longer becomes `boxalphabetic`) ([#20392](https://github.com/tailwindlabs/tailwindcss/pull/20392)) +- Ensure custom named `--spacing-*` theme values (e.g. `--spacing-none`) don't shadow keyword values of spacing utilities like `leading-none` ([#20394](https://github.com/tailwindlabs/tailwindcss/pull/20394)) ## [4.3.3] - 2026-07-16 diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index c9d4a5ff0dfa..1a46438e36af 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -25727,6 +25727,86 @@ test('leading', async () => { } " `) + + // Custom named --spacing-* values should not shadow the none keyword but remain resolvable by name + expect( + await run( + ['leading-none', 'leading-big'], + css` + @theme { + --spacing-none: 0; + --spacing-big: 3rem; + } + @tailwind utilities; + `, + ), + ).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-leading: initial; + } + } + } + + :root, :host { + --spacing-big: 3rem; + } + + .leading-big { + --tw-leading: var(--spacing-big); + line-height: var(--spacing-big); + } + + .leading-none { + --tw-leading: 1; + line-height: 1; + } + + @property --tw-leading { + syntax: "*"; + inherits: false + } + " + `) + + expect( + await run( + ['leading-none'], + css` + @theme { + --leading-none: 1.5; + --spacing-none: 0; + } + @tailwind utilities; + `, + ), + ).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-leading: initial; + } + } + } + + :root, :host { + --leading-none: 1.5; + } + + .leading-none { + --tw-leading: var(--leading-none); + line-height: var(--leading-none); + } + + @property --tw-leading { + syntax: "*"; + inherits: false + } + " + `) }) test('tracking', async () => { diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 6c4a20cbc847..4c9192873a15 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -381,6 +381,7 @@ export function createUtilities(theme: Theme) { supportsNegative?: boolean supportsFractions?: boolean themeKeys?: ThemeKey[] + fallbackThemeKeys?: ThemeKey[] defaultValue?: string | null staticValues?: Record handleBareValue?: (value: NamedUtilityValue) => string | null @@ -421,6 +422,19 @@ export function createUtilities(theme: Theme) { desc.themeKeys ?? [], ) + if (value === null && desc.fallbackThemeKeys) { + // Static keywords like leading-none win over fallback namespaces like --spacing + if (!negative && desc.staticValues && !candidate.modifier) { + let fallback = desc.staticValues[candidate.value.value] + if (fallback) return fallback.map(cloneAstNode) + } + + value = theme.resolve( + candidate.value.fraction ?? candidate.value.value, + desc.fallbackThemeKeys, + ) + } + // Automatically handle things like `w-1/2` without requiring `1/2` to // exist as a theme value. if (value === null && desc.supportsFractions && candidate.value.fraction) { @@ -467,7 +481,7 @@ export function createUtilities(theme: Theme) { suggest(classRoot, () => [ { supportsNegative: desc.supportsNegative, - valueThemeKeys: desc.themeKeys ?? [], + valueThemeKeys: [...(desc.themeKeys ?? []), ...(desc.fallbackThemeKeys ?? [])], hasDefaultValue: desc.defaultValue !== undefined && desc.defaultValue !== null, supportsFractions: desc.supportsFractions, }, @@ -543,8 +557,17 @@ export function createUtilities(theme: Theme) { utilities.static(`-${name}-px`, () => handle('-1px')) } utilities.static(`${name}-px`, () => handle('1px')) + + // The generic `--spacing` namespace (and everything after it) is resolved + // only after `staticValues`, so custom values like `--spacing-none` don't + // shadow canonical keywords like `leading-none`. + let fallbackIndex = themeKeys.indexOf('--spacing') + let ownThemeKeys = fallbackIndex === -1 ? themeKeys : themeKeys.slice(0, fallbackIndex) + let fallbackThemeKeys = fallbackIndex === -1 ? undefined : themeKeys.slice(fallbackIndex) + functionalUtility(name, { - themeKeys, + themeKeys: ownThemeKeys, + fallbackThemeKeys, supportsFractions, supportsNegative, defaultValue: null,