diff --git a/CHANGELOG.md b/CHANGELOG.md index 004bacef85de..faf6c8cc407c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added new `bg-{position,size}-*` utilities for arbitrary values ([#17432](https://github.com/tailwindlabs/tailwindcss/pull/17432)) - Added new `shadow-*/{alpha}`, `inset-shadow-*/{alpha}`, and `text-shadow-*/{alpha}` utilities to control shadow opacity ([#17398](https://github.com/tailwindlabs/tailwindcss/pull/17398)) - Added new `object-{top,bottom}-{left,right}` utilities ([#17437](https://github.com/tailwindlabs/tailwindcss/pull/17437)) +- Added new `drop-shadow-{color}` utilities ([#17434](https://github.com/tailwindlabs/tailwindcss/pull/17434)) +- Added new `drop-shadow-*/{alpha}` utilities to control drop shadow opacity ([#17434](https://github.com/tailwindlabs/tailwindcss/pull/17434)) ### Fixed diff --git a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap index 2a8c9eae3bc1..511b880e70b5 100644 --- a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap +++ b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap @@ -3984,8 +3984,74 @@ exports[`getClassList 1`] = ` "divide-y-4", "divide-y-8", "divide-y-reverse", + "drop-shadow-current", + "drop-shadow-current/0", + "drop-shadow-current/5", + "drop-shadow-current/10", + "drop-shadow-current/15", + "drop-shadow-current/20", + "drop-shadow-current/25", + "drop-shadow-current/30", + "drop-shadow-current/35", + "drop-shadow-current/40", + "drop-shadow-current/45", + "drop-shadow-current/50", + "drop-shadow-current/55", + "drop-shadow-current/60", + "drop-shadow-current/65", + "drop-shadow-current/70", + "drop-shadow-current/75", + "drop-shadow-current/80", + "drop-shadow-current/85", + "drop-shadow-current/90", + "drop-shadow-current/95", + "drop-shadow-current/100", + "drop-shadow-inherit", + "drop-shadow-inherit/0", + "drop-shadow-inherit/5", + "drop-shadow-inherit/10", + "drop-shadow-inherit/15", + "drop-shadow-inherit/20", + "drop-shadow-inherit/25", + "drop-shadow-inherit/30", + "drop-shadow-inherit/35", + "drop-shadow-inherit/40", + "drop-shadow-inherit/45", + "drop-shadow-inherit/50", + "drop-shadow-inherit/55", + "drop-shadow-inherit/60", + "drop-shadow-inherit/65", + "drop-shadow-inherit/70", + "drop-shadow-inherit/75", + "drop-shadow-inherit/80", + "drop-shadow-inherit/85", + "drop-shadow-inherit/90", + "drop-shadow-inherit/95", + "drop-shadow-inherit/100", "drop-shadow-none", "drop-shadow-sm", + "drop-shadow-transparent", + "drop-shadow-transparent/0", + "drop-shadow-transparent/5", + "drop-shadow-transparent/10", + "drop-shadow-transparent/15", + "drop-shadow-transparent/20", + "drop-shadow-transparent/25", + "drop-shadow-transparent/30", + "drop-shadow-transparent/35", + "drop-shadow-transparent/40", + "drop-shadow-transparent/45", + "drop-shadow-transparent/50", + "drop-shadow-transparent/55", + "drop-shadow-transparent/60", + "drop-shadow-transparent/65", + "drop-shadow-transparent/70", + "drop-shadow-transparent/75", + "drop-shadow-transparent/80", + "drop-shadow-transparent/85", + "drop-shadow-transparent/90", + "drop-shadow-transparent/95", + "drop-shadow-transparent/100", "duration-75", "duration-100", "duration-150", diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 2be51cbb9ecd..5e2bf3eb5a47 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -20362,6 +20362,7 @@ test('filter', async () => { css` @theme { --blur-xl: 24px; + --color-red-500: #ef4444; --drop-shadow: 0 1px 1px rgb(0 0 0 / 0.05); --drop-shadow-xl: 0 9px 7px rgb(0 0 0 / 0.1); } @@ -20389,8 +20390,11 @@ test('filter', async () => { 'invert-0', 'invert-[var(--value)]', 'drop-shadow', + 'drop-shadow/25', 'drop-shadow-xl', 'drop-shadow-[0_0_red]', + 'drop-shadow-red-500', + 'drop-shadow-red-500/50', 'saturate-0', 'saturate-[1.75]', 'saturate-[var(--value)]', @@ -20414,12 +20418,16 @@ test('filter', async () => { --tw-saturate: initial; --tw-sepia: initial; --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; } } } :root, :host { --blur-xl: 24px; + --color-red-500: #ef4444; --drop-shadow: 0 1px 1px #0000000d; --drop-shadow-xl: 0 9px 7px #0000001a; } @@ -20459,21 +20467,53 @@ test('filter', async () => { filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ); } + .drop-shadow\\/25 { + --tw-drop-shadow-alpha: 25%; + --tw-drop-shadow-size: drop-shadow(0 1px 1px var(--tw-drop-shadow-color, oklab(0% 0 0 / .25))); + --tw-drop-shadow: drop-shadow(var(--drop-shadow)); + filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ); + } + .drop-shadow { + --tw-drop-shadow-size: drop-shadow(0 1px 1px var(--tw-drop-shadow-color, #0000000d)); --tw-drop-shadow: drop-shadow(var(--drop-shadow)); filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ); } .drop-shadow-\\[0_0_red\\] { - --tw-drop-shadow: drop-shadow(0 0 red); + --tw-drop-shadow-size: drop-shadow(0 0 var(--tw-drop-shadow-color, red)); + --tw-drop-shadow: var(--tw-drop-shadow-size); filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ); } .drop-shadow-xl { + --tw-drop-shadow-size: drop-shadow(0 9px 7px var(--tw-drop-shadow-color, #0000001a)); --tw-drop-shadow: drop-shadow(var(--drop-shadow-xl)); filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ); } + .drop-shadow-red-500 { + --tw-drop-shadow-color: color-mix(in srgb, #ef4444 var(--tw-drop-shadow-alpha), transparent); + --tw-drop-shadow: var(--tw-drop-shadow-size); + } + + @supports (color: color-mix(in lab, red, red)) { + .drop-shadow-red-500 { + --tw-drop-shadow-color: color-mix(in oklab, var(--color-red-500) var(--tw-drop-shadow-alpha), transparent); + } + } + + .drop-shadow-red-500\\/50 { + --tw-drop-shadow-color: color-mix(in srgb, #ef444480 var(--tw-drop-shadow-alpha), transparent); + --tw-drop-shadow: var(--tw-drop-shadow-size); + } + + @supports (color: color-mix(in lab, red, red)) { + .drop-shadow-red-500\\/50 { + --tw-drop-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-red-500) 50%, transparent) var(--tw-drop-shadow-alpha), transparent); + } + } + .grayscale { --tw-grayscale: grayscale(100%); filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ); @@ -20619,6 +20659,22 @@ test('filter', async () => { @property --tw-drop-shadow { syntax: "*"; inherits: false + } + + @property --tw-drop-shadow-color { + syntax: "*"; + inherits: false + } + + @property --tw-drop-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; + } + + @property --tw-drop-shadow-size { + syntax: "*"; + inherits: false }" `) expect( @@ -20650,6 +20706,15 @@ test('filter', async () => { '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', + '-saturate-0', 'saturate--5', '-saturate-[1.75]', diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index ab3696d3b4c2..b688fdd41071 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -3926,6 +3926,9 @@ export function createUtilities(theme: Theme) { property('--tw-saturate'), property('--tw-sepia'), property('--tw-drop-shadow'), + property('--tw-drop-shadow-color'), + property('--tw-drop-shadow-alpha', '100%', ''), + property('--tw-drop-shadow-size'), ]) } @@ -4318,20 +4321,134 @@ export function createUtilities(theme: Theme) { ['--tw-drop-shadow', ' '], ['filter', cssFilterValue], ]) - functionalUtility('drop-shadow', { - themeKeys: ['--drop-shadow'], - handle: (value) => [ - filterProperties(), - decl( - '--tw-drop-shadow', - segment(value, ',') - .map((v) => `drop-shadow(${v})`) - .join(' '), - ), - decl('filter', cssFilterValue), - ], + + utilities.functional('drop-shadow', (candidate) => { + let alpha: string | undefined + + if (candidate.modifier) { + if (candidate.modifier.kind === 'arbitrary') { + alpha = candidate.modifier.value + } else { + if (isPositiveInteger(candidate.modifier.value)) { + alpha = `${candidate.modifier.value}%` + } + } + } + + if (!candidate.value) { + let value = theme.get(['--drop-shadow']) + if (value === null) return + + return [ + filterProperties(), + decl('--tw-drop-shadow-alpha', alpha), + ...alphaReplacedDropShadowProperties( + '--tw-drop-shadow-size', + value, + alpha, + (color) => `var(--tw-drop-shadow-color, ${color})`, + ), + decl('--tw-drop-shadow', `drop-shadow(${theme.resolve(null, ['--drop-shadow'])})`), + decl('filter', cssFilterValue), + ] + } + + if (candidate.value.kind === 'arbitrary') { + let value: string | null = candidate.value.value + let type = candidate.value.dataType ?? inferDataType(value, ['color']) + + switch (type) { + case 'color': { + value = asColor(value, candidate.modifier, theme) + if (value === null) return + return [ + filterProperties(), + decl('--tw-drop-shadow-color', withAlpha(value, 'var(--tw-drop-shadow-alpha)')), + decl('--tw-drop-shadow', `var(--tw-drop-shadow-size)`), + ] + } + default: { + if (candidate.modifier && !alpha) return + + return [ + filterProperties(), + decl('--tw-drop-shadow-alpha', alpha), + ...alphaReplacedDropShadowProperties( + '--tw-drop-shadow-size', + value, + alpha, + (color) => `var(--tw-drop-shadow-color, ${color})`, + ), + decl('--tw-drop-shadow', `var(--tw-drop-shadow-size)`), + decl('filter', cssFilterValue), + ] + } + } + } + + // Shadow size + { + let value = theme.get([`--drop-shadow-${candidate.value.value}`]) + if (value) { + if (candidate.modifier && !alpha) return + + if (alpha) { + return [ + filterProperties(), + decl('--tw-drop-shadow-alpha', alpha), + ...alphaReplacedDropShadowProperties( + '--tw-drop-shadow-size', + value, + alpha, + (color) => `var(--tw-drop-shadow-color, ${color})`, + ), + decl('--tw-drop-shadow', `var(--tw-drop-shadow-size)`), + decl('filter', cssFilterValue), + ] + } + + return [ + filterProperties(), + decl('--tw-drop-shadow-alpha', alpha), + ...alphaReplacedDropShadowProperties( + '--tw-drop-shadow-size', + value, + alpha, + (color) => `var(--tw-drop-shadow-color, ${color})`, + ), + decl( + '--tw-drop-shadow', + `drop-shadow(${theme.resolve(candidate.value.value, ['--drop-shadow'])})`, + ), + decl('filter', cssFilterValue), + ] + } + } + + // Shadow color + { + let value = resolveThemeColor(candidate, theme, ['--drop-shadow-color', '--color']) + if (value) { + return [ + filterProperties(), + decl('--tw-drop-shadow-color', withAlpha(value, 'var(--tw-drop-shadow-alpha)')), + decl('--tw-drop-shadow', `var(--tw-drop-shadow-size)`), + ] + } + } }) + suggest('drop-shadow', () => [ + { + values: ['current', 'inherit', 'transparent'], + valueThemeKeys: ['--drop-shadow-color', '--color'], + modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`), + }, + { + valueThemeKeys: ['--drop-shadow'], + }, + ]) + functionalUtility('backdrop-opacity', { themeKeys: ['--backdrop-opacity', '--opacity'], handleBareValue: ({ value }) => { @@ -6116,3 +6233,54 @@ function alphaReplacedShadowProperties( return [decl(property, prefix + replacedValue)] } } + +function alphaReplacedDropShadowProperties( + property: string, + value: string, + alpha: string | null | undefined, + varInjector: (color: string) => string, + prefix: string = '', +): AstNode[] { + let requiresFallback = false + + let replacedValue = segment(value, ',') + .map((value) => + replaceShadowColors(value, (color) => { + if (alpha == null) { + return varInjector(color) + } + + // When the input is currentcolor, we use our existing `color-mix(…)` approach to increase + // browser support. Note that the fallback of this is handled more generically in + // post-processing. + if (color.startsWith('current')) { + return varInjector(withAlpha(color, alpha)) + } + + // If any dynamic values are needed for the relative color syntax, we need to insert a + // replacement as lightningcss won't be able to resolve them statically. + if (color.startsWith('var(') || alpha.startsWith('var(')) { + requiresFallback = true + } + + return varInjector(replaceAlpha(color, alpha)) + }), + ) + .map((value) => `drop-shadow(${value})`) + .join(' ') + + if (requiresFallback) { + return [ + decl( + property, + prefix + + segment(value, ',') + .map((value) => `drop-shadow(${replaceShadowColors(value, varInjector)})`) + .join(' '), + ), + rule('@supports (color: lab(from red l a b))', [decl(property, prefix + replacedValue)]), + ] + } else { + return [decl(property, prefix + replacedValue)] + } +} diff --git a/packages/tailwindcss/tests/ui.spec.ts b/packages/tailwindcss/tests/ui.spec.ts index a62a9306bfd2..9aaf0265ab8e 100644 --- a/packages/tailwindcss/tests/ui.spec.ts +++ b/packages/tailwindcss/tests/ui.spec.ts @@ -1826,6 +1826,43 @@ test('filter', async ({ page }) => { expect(await getPropertyValue('#b', 'filter')).toEqual('contrast(1)') }) +test('drop shadow colors', async ({ page }) => { + let { getPropertyList } = await render( + page, + html` +
+
+
+
+
+ `, + ) + + expect(await getPropertyList('#a', 'filter')).toEqual([ + 'drop-shadow(rgba(0, 0, 0, 0.12) 0px 3px 3px)', + ]) + + expect(await getPropertyList('#b', 'filter')).toEqual([ + expect.stringMatching(/drop-shadow\(oklab\(0\.627\d+ 0\.224\d+ 0\.125\d+\) 0px 3px 3px\)/), + ]) + + expect(await getPropertyList('#c', 'filter')).toEqual([ + 'drop-shadow(oklab(0 0 0 / 0.5) 0px 3px 3px)', + ]) + + expect(await getPropertyList('#d', 'filter')).toEqual([ + expect.stringMatching( + /drop-shadow\(oklab\(0\.627\d+ 0\.224\d+ 0\.125\d+ \/ 0\.5\) 0px 3px 3px\)/, + ), + ]) + + expect(await getPropertyList('#e', 'filter')).toEqual([ + expect.stringMatching( + /drop-shadow\(oklab\(0\.627\d+ 0\.224\d+ 0\.125\d+ \/ 0\.25\) 0px 3px 3px\)/, + ), + ]) +}) + test('outline style is optional', async ({ page }) => { let { getPropertyValue } = await render( page,