Add drop-shadow-* color support (#17434)

This PR adds support for two things:

- New `drop-shadow-{color}` utilities which can be used to control the
filder drop shadow color
- New `drop-shadow-*/{alpha}` utilities which can be used to control the
intensity of the drop shadow

Note that drop-shadow-* utilities without a modifier use variables from
your theme.

If you opt into using drop shadow colors or drop-shadow "intensity"
utilities we will inline these values into your CSS when using a drop
shadow color or drop shadow intensity utility and as such can't be
changed by changing the theme variables on a per-element/tree basis.

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
Jordan Pittman 2025-04-01 11:11:49 -04:00 • committed by GitHub
parent 55d7a65cfc
commit 9374647b86
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 351 additions and 13 deletions

View file

@ -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

View file

@ -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",

View file

@ -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: "<percentage>";
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]',

View file

@ -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%', '<percentage>'),
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)]
}
}

View file

@ -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`
<div id="a" class="drop-shadow-md"></div>
<div id="b" class="drop-shadow-md drop-shadow-red"></div>
<div id="c" class="drop-shadow-md/50"></div>
<div id="d" class="drop-shadow-md/50 drop-shadow-red"></div>
<div id="e" class="drop-shadow-md/50 drop-shadow-red/50"></div>
`,
)
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,