diff --git a/CHANGELOG.md b/CHANGELOG.md index 175a1daa2..8d59cd834 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Oxide: drop invalid UTF-8 candidates ([#20389](https://github.com/tailwindlabs/tailwindcss/pull/20389)) - `@tailwindcss/vite` no longer forces a full page reload for external files (e.g.: `.php` files) ([#20414](https://github.com/tailwindlabs/tailwindcss/issues/20414)) - Canonicalization: don't merge utilities that reference different theme variables set to CSS-wide keywords like `unset` ([#20417](https://github.com/tailwindlabs/tailwindcss/pull/20417)) +- Don't generate utilities when a modifier is used that would otherwise be silently ignored (e.g. `rounded-sm/[5]`, `shadow-sm/foo`, `stroke-2/50`) ([#20419](https://github.com/tailwindlabs/tailwindcss/pull/20419)) ## [4.3.3] - 2026-07-16 diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index c9d4a5ff0..66b051d57 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -12154,18 +12154,28 @@ test('rounded', async () => { " `) expect( - await run([ - '-rounded', - '-rounded-full', - '-rounded-none', - '-rounded-sm', - '-rounded-[4px]', - 'rounded/foo', - 'rounded-full/foo', - 'rounded-none/foo', - 'rounded-sm/foo', - 'rounded-[4px]/foo', - ]), + await run( + [ + '-rounded', + '-rounded-full', + '-rounded-none', + '-rounded-sm', + '-rounded-[4px]', + 'rounded/foo', + 'rounded-full/foo', + 'rounded-none/foo', + 'rounded-sm/foo', + 'rounded-[4px]/foo', + 'rounded-sm/[5]', + 'rounded-sm/(--foo)', + ], + css` + @theme { + --radius-sm: 0.125rem; + } + @tailwind utilities; + `, + ), ).toEqual('') }) @@ -22373,6 +22383,11 @@ test('stroke', async () => { // Width '-stroke-0', 'stroke--1', + 'stroke-0/foo', + 'stroke-1/50', + 'stroke-2/[0.5]', + 'stroke-2/(--my-opacity)', + 'stroke-[12px]/foo', ]), ).toEqual('') }) @@ -24398,82 +24413,90 @@ test('filter', async () => { " `) expect( - await run([ - '-filter', - '-filter-none', - '-filter-[var(--value)]', - '-blur-xl', - '-blur-[4px]', - 'brightness--50', - '-brightness-50', - '-brightness-[1.23]', - 'brightness-unknown', - 'contrast--50', - '-contrast-50', - '-contrast-[1.23]', - 'contrast-unknown', - '-grayscale', - '-grayscale-0', - 'grayscale--1', - '-grayscale-[var(--value)]', - 'grayscale-unknown', - 'hue-rotate--5', - 'hue-rotate-unknown', - '-invert', - 'invert--5', - '-invert-0', - '-invert-[var(--value)]', - 'invert-unknown', - '-drop-shadow-xl', - '-drop-shadow-[0_0_red]', + await run( + [ + '-filter', + '-filter-none', + '-filter-[var(--value)]', + '-blur-xl', + '-blur-[4px]', + 'brightness--50', + '-brightness-50', + '-brightness-[1.23]', + 'brightness-unknown', + 'contrast--50', + '-contrast-50', + '-contrast-[1.23]', + 'contrast-unknown', + '-grayscale', + '-grayscale-0', + 'grayscale--1', + '-grayscale-[var(--value)]', + 'grayscale-unknown', + 'hue-rotate--5', + 'hue-rotate-unknown', + '-invert', + 'invert--5', + '-invert-0', + '-invert-[var(--value)]', + 'invert-unknown', + '-drop-shadow-xl', + '-drop-shadow-[0_0_red]', - 'drop-shadow/foo', - '-drop-shadow/foo', - '-drop-shadow/25', - '-drop-shadow-red-500', - 'drop-shadow-red-500/foo', - '-drop-shadow-red-500/foo', - '-drop-shadow-red-500/50', + 'drop-shadow/foo', + '-drop-shadow/foo', + '-drop-shadow/25', + '-drop-shadow-red-500', + 'drop-shadow-red-500/foo', + '-drop-shadow-red-500/foo', + '-drop-shadow-red-500/50', - '-saturate-0', - 'saturate--5', - '-saturate-[1.75]', - '-saturate-[var(--value)]', - 'saturate-saturate', - '-sepia', - 'sepia--50', - '-sepia-0', - '-sepia-[50%]', - '-sepia-[var(--value)]', - 'sepia-unknown', - 'filter/foo', - 'filter-none/foo', - 'filter-[var(--value)]/foo', - 'blur-xl/foo', - 'blur-none/foo', - 'blur-[4px]/foo', - 'brightness-50/foo', - 'brightness-[1.23]/foo', - 'contrast-50/foo', - 'contrast-[1.23]/foo', - 'grayscale/foo', - 'grayscale-0/foo', - 'grayscale-[var(--value)]/foo', - 'hue-rotate-15/foo', - 'hue-rotate-[45deg]/foo', - 'invert/foo', - 'invert-0/foo', - 'invert-[var(--value)]/foo', - 'drop-shadow-xl/foo', - 'drop-shadow-[0_0_red]/foo', - 'saturate-0/foo', - 'saturate-[1.75]/foo', - 'saturate-[var(--value)]/foo', - 'sepia/foo', - 'sepia-0/foo', - 'sepia-[50%]/foo', - 'sepia-[var(--value)]/foo', - ]), + '-saturate-0', + 'saturate--5', + '-saturate-[1.75]', + '-saturate-[var(--value)]', + 'saturate-saturate', + '-sepia', + 'sepia--50', + '-sepia-0', + '-sepia-[50%]', + '-sepia-[var(--value)]', + 'sepia-unknown', + 'filter/foo', + 'filter-none/foo', + 'filter-[var(--value)]/foo', + 'blur-xl/foo', + 'blur-none/foo', + 'blur-[4px]/foo', + 'brightness-50/foo', + 'brightness-[1.23]/foo', + 'contrast-50/foo', + 'contrast-[1.23]/foo', + 'grayscale/foo', + 'grayscale-0/foo', + 'grayscale-[var(--value)]/foo', + 'hue-rotate-15/foo', + 'hue-rotate-[45deg]/foo', + 'invert/foo', + 'invert-0/foo', + 'invert-[var(--value)]/foo', + 'drop-shadow-xl/foo', + 'drop-shadow-[0_0_red]/foo', + 'saturate-0/foo', + 'saturate-[1.75]/foo', + 'saturate-[var(--value)]/foo', + 'sepia/foo', + 'sepia-0/foo', + 'sepia-[50%]/foo', + 'sepia-[var(--value)]/foo', + ], + css` + @theme { + --drop-shadow: 0 1px 1px rgb(0 0 0 / 0.05); + } + @tailwind utilities; + `, + ), ).toEqual('') expect( @@ -27243,25 +27266,36 @@ test('text-shadow', async () => { " `) expect( - await run([ - '-shadow-xl', - '-shadow-none', - '-shadow-red-500', - '-shadow-red-500/50', - '-shadow-red-500/[0.5]', - '-shadow-red-500/[50%]', - '-shadow-current', - '-shadow-current/50', - '-shadow-current/[0.5]', - '-shadow-current/[50%]', - '-shadow-inherit', - '-shadow-transparent', - '-shadow-[#0088cc]', - '-shadow-[#0088cc]/50', - '-shadow-[#0088cc]/[0.5]', - '-shadow-[#0088cc]/[50%]', - '-shadow-[var(--value)]', - ]), + await run( + [ + '-text-shadow-sm', + '-text-shadow-none', + '-text-shadow-red-500', + '-text-shadow-red-500/50', + '-text-shadow-red-500/[0.5]', + '-text-shadow-red-500/[50%]', + '-text-shadow-current', + '-text-shadow-current/50', + '-text-shadow-current/[0.5]', + '-text-shadow-current/[50%]', + '-text-shadow-inherit', + '-text-shadow-transparent', + '-text-shadow-[#0088cc]', + '-text-shadow-[#0088cc]/50', + '-text-shadow-[#0088cc]/[0.5]', + '-text-shadow-[#0088cc]/[50%]', + '-text-shadow-[var(--value)]', + 'text-shadow/foo', + 'text-shadow-sm/foo', + ], + css` + @theme { + --text-shadow: 0 1px 1px rgb(0 0 0 / 0.05); + --text-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.06); + } + @tailwind utilities; + `, + ), ).toEqual('') }) @@ -27725,25 +27759,38 @@ test('shadow', async () => { " `) expect( - await run([ - '-shadow-xl', - '-shadow-none', - '-shadow-red-500', - '-shadow-red-500/50', - '-shadow-red-500/[0.5]', - '-shadow-red-500/[50%]', - '-shadow-current', - '-shadow-current/50', - '-shadow-current/[0.5]', - '-shadow-current/[50%]', - '-shadow-inherit', - '-shadow-transparent', - '-shadow-[#0088cc]', - '-shadow-[#0088cc]/50', - '-shadow-[#0088cc]/[0.5]', - '-shadow-[#0088cc]/[50%]', - '-shadow-[var(--value)]', - ]), + await run( + [ + '-shadow-sm', + '-shadow-none', + '-shadow-red-500', + '-shadow-red-500/50', + '-shadow-red-500/[0.5]', + '-shadow-red-500/[50%]', + '-shadow-current', + '-shadow-current/50', + '-shadow-current/[0.5]', + '-shadow-current/[50%]', + '-shadow-inherit', + '-shadow-transparent', + '-shadow-[#0088cc]', + '-shadow-[#0088cc]/50', + '-shadow-[#0088cc]/[0.5]', + '-shadow-[#0088cc]/[50%]', + '-shadow-[var(--value)]', + 'shadow/foo', + 'shadow-sm/foo', + 'shadow-[10px_10px]/foo', + 'shadow-[#0088cc]/foo', + ], + css` + @theme { + --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); + --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + } + @tailwind utilities; + `, + ), ).toEqual('') }) @@ -28216,25 +28263,37 @@ test('inset-shadow', async () => { " `) expect( - await run([ - '-inset-shadow-sm', - '-inset-shadow-none', - '-inset-shadow-red-500', - '-inset-shadow-red-500/50', - '-inset-shadow-red-500/[0.5]', - '-inset-shadow-red-500/[50%]', - '-inset-shadow-current', - '-inset-shadow-current/50', - '-inset-shadow-current/[0.5]', - '-inset-shadow-current/[50%]', - '-inset-shadow-inherit', - '-inset-shadow-transparent', - '-inset-shadow-[#0088cc]', - '-inset-shadow-[#0088cc]/50', - '-inset-shadow-[#0088cc]/[0.5]', - '-inset-shadow-[#0088cc]/[50%]', - '-inset-shadow-[var(--value)]', - ]), + await run( + [ + '-inset-shadow-sm', + '-inset-shadow-none', + '-inset-shadow-red-500', + '-inset-shadow-red-500/50', + '-inset-shadow-red-500/[0.5]', + '-inset-shadow-red-500/[50%]', + '-inset-shadow-current', + '-inset-shadow-current/50', + '-inset-shadow-current/[0.5]', + '-inset-shadow-current/[50%]', + '-inset-shadow-inherit', + '-inset-shadow-transparent', + '-inset-shadow-[#0088cc]', + '-inset-shadow-[#0088cc]/50', + '-inset-shadow-[#0088cc]/[0.5]', + '-inset-shadow-[#0088cc]/[50%]', + '-inset-shadow-[var(--value)]', + 'inset-shadow/foo', + 'inset-shadow-sm/foo', + 'inset-shadow-[10px_10px]/foo', + ], + css` + @theme { + --inset-shadow: inset 0 1px 3px 0 rgb(0 0 0 / 0.1); + --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); + } + @tailwind utilities; + `, + ), ).toEqual('') }) diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 6c4a20cbc..477559d7e 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -421,6 +421,13 @@ export function createUtilities(theme: Theme) { desc.themeKeys ?? [], ) + // If the theme value resolved without consuming the modifier, then + // the modifier would be silently ignored and `w-4` and `w-4/foo` + // would generate the same CSS. + // + // The modifier would be consumed if it's a fraction, e.g.: `w-1/2` + if (value !== null && candidate.modifier && !candidate.value.fraction) return + // 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) { @@ -3888,6 +3895,10 @@ export function createUtilities(theme: Theme) { } { + // A modifier is only valid as the opacity of a stroke color. The value + // did not resolve to a color, so a modifier would be silently ignored. + if (candidate.modifier) return + let value = theme.resolve(candidate.value.value, ['--stroke-width']) if (value) { return [decl('stroke-width', value)] @@ -4628,6 +4639,7 @@ export function createUtilities(theme: Theme) { let value = theme.get(['--drop-shadow']) let resolved = theme.resolve(null, ['--drop-shadow']) if (value === null || resolved === null) return + if (candidate.modifier && !alpha) return return [ filterProperties(), @@ -5436,6 +5448,7 @@ export function createUtilities(theme: Theme) { if (!candidate.value) { let value = theme.get(['--text-shadow']) if (value === null) return + if (candidate.modifier && !alpha) return return [ textShadowProperties(), @@ -5463,6 +5476,8 @@ export function createUtilities(theme: Theme) { ] } default: { + if (candidate.modifier && !alpha) return + return [ textShadowProperties(), decl('--tw-text-shadow-alpha', alpha), @@ -5491,6 +5506,8 @@ export function createUtilities(theme: Theme) { { let value = theme.get([`--text-shadow-${candidate.value.value}`]) if (value) { + if (candidate.modifier && !alpha) return + return [ textShadowProperties(), decl('--tw-text-shadow-alpha', alpha), @@ -5582,6 +5599,7 @@ export function createUtilities(theme: Theme) { if (!candidate.value) { let value = theme.get(['--shadow']) if (value === null) return + if (candidate.modifier && !alpha) return return [ boxShadowProperties(), @@ -5611,6 +5629,8 @@ export function createUtilities(theme: Theme) { ] } default: { + if (candidate.modifier && !alpha) return + return [ boxShadowProperties(), decl('--tw-shadow-alpha', alpha), @@ -5644,6 +5664,8 @@ export function createUtilities(theme: Theme) { { let value = theme.get([`--shadow-${candidate.value.value}`]) if (value) { + if (candidate.modifier && !alpha) return + return [ boxShadowProperties(), decl('--tw-shadow-alpha', alpha), @@ -5708,6 +5730,7 @@ export function createUtilities(theme: Theme) { if (!candidate.value) { let value = theme.get(['--inset-shadow']) if (value === null) return + if (candidate.modifier && !alpha) return return [ boxShadowProperties(), @@ -5737,6 +5760,8 @@ export function createUtilities(theme: Theme) { ] } default: { + if (candidate.modifier && !alpha) return + return [ boxShadowProperties(), decl('--tw-inset-shadow-alpha', alpha), @@ -5772,6 +5797,8 @@ export function createUtilities(theme: Theme) { let value = theme.get([`--inset-shadow-${candidate.value.value}`]) if (value) { + if (candidate.modifier && !alpha) return + return [ boxShadowProperties(), decl('--tw-inset-shadow-alpha', alpha),