Support fractional opacity modifiers for named shadow sizes (#20302)
## Summary `shadow-*`, `text-shadow-*`, `drop-shadow-*`, and `inset-shadow-*` accept a bare (non-arbitrary) opacity modifier like `/50`, but the named-size branch of all four utilities validates it with `isPositiveInteger(candidate.modifier.value)` instead of `isValidOpacityValue(candidate.modifier.value)` — the helper every other opacity/alpha modifier in the codebase uses (`asColor`, used by `bg-*`, `text-*`, `border-*`, `ring-*`, `fill-*`, `stroke-*`, `decoration-*`, `accent-*`, `caret-*`, `outline-*`, `placeholder-*`, `divide-*`). This means a fractional modifier like `/12.5` is silently ignored for a *named size* (`shadow-sm/12.5` behaves exactly like `shadow-sm`, dropping the modifier), while the exact same `/12.5` modifier works correctly on the *color* variant of the same utility (`shadow-red-500/12.5` → `color-mix(in oklab, var(--color-red-500) 12.5%, transparent)`), since that path already goes through `asColor`. `drop-shadow-*` is worse: its named-size branch has an extra guard (`if (candidate.modifier && !alpha) return`) that bails out of the *entire* utility when a modifier is present but couldn't be resolved to an alpha — so `drop-shadow-sm/12.5` produces no CSS at all. This isn't a case of fractional percentages being unsupported by design — the CHANGELOG entry that introduced `shadow-*/<alpha>` explicitly describes it as controlling shadow **opacity**, and the color branch of these same utilities already supports fractional values (`shadow-red-500/2.25`, `/2.5`, `/2.75` are covered by existing tests). The named-size branch just never got the same treatment. **Repro** (verified with `pnpm --filter tailwindcss exec vitest run`): - `shadow-red-500/12.5` → `color-mix(in oklab, var(--color-red-500) 12.5%, transparent)` (correct) - `shadow-sm/12.5` → identical output to plain `shadow-sm` (modifier silently dropped) - `drop-shadow-sm/12.5` → **no CSS generated at all** - `shadow-sm/50` (integer, control) → works correctly ## Fix Replaced `isPositiveInteger(candidate.modifier.value)` with `isValidOpacityValue(candidate.modifier.value)` in the four affected utility definitions in `packages/tailwindcss/src/utilities.ts` (`shadow`, `text-shadow`, `drop-shadow`, `inset-shadow`). No other changes were needed — once `alpha` resolves correctly, `drop-shadow`'s existing `if (candidate.modifier && !alpha) return` guard naturally stops bailing out, since `alpha` is no longer `undefined` for valid fractional modifiers. ## Test plan - Added a regression test in `packages/tailwindcss/src/utilities.test.ts` covering `shadow-sm/12.5`, `text-shadow-sm/12.5`, `drop-shadow-sm/12.5`, and `inset-shadow-sm/12.5`. - Verified via `pnpm --filter tailwindcss exec vitest run` that this test fails (modifier dropped / empty output) with the fix reverted, and passes with it applied. - Ran the full `tailwindcss` package test suite (`pnpm --filter tailwindcss exec vitest run`) — 4685 tests passing, no regressions. - Verified formatting on the changed files with `npx prettier --check`. --------- Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
parent
04588b1e8f
commit
2683903b86
3 changed files with 33 additions and 4 deletions
|
|
@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Canonicalization: match arbitrary hex colors against theme colors case-insensitively (e.g. `bg-[#fff]` and `bg-[#FFF]` → `bg-white`) ([#20298](https://github.com/tailwindlabs/tailwindcss/pull/20298))
|
||||
- Prevent Preflight from overriding Firefox's native `iframe:focus-visible` outline styles ([#20292](https://github.com/tailwindlabs/tailwindcss/pull/20292))
|
||||
- Prevent `theme('colors.foo')` in JS plugins from returning an internal disambiguation object when a CSS theme key shares a prefix with a sibling key like `--color-foo-bar` ([#20299](https://github.com/tailwindlabs/tailwindcss/pull/20299))
|
||||
- Ensure fractional opacity modifiers work with named shadow sizes like `shadow-sm/12.5`, `text-shadow-sm/12.5`, `drop-shadow-sm/12.5`, and `inset-shadow-sm/12.5` ([#20302](https://github.com/tailwindlabs/tailwindcss/pull/20302))
|
||||
|
||||
## [4.3.2] - 2026-06-26
|
||||
|
||||
|
|
|
|||
|
|
@ -24053,6 +24053,7 @@ test('filter', async () => {
|
|||
'invert-[var(--value)]',
|
||||
'drop-shadow',
|
||||
'drop-shadow/25',
|
||||
'drop-shadow/12.5',
|
||||
'drop-shadow-xl',
|
||||
'drop-shadow-multi',
|
||||
'drop-shadow-[0_0_red]',
|
||||
|
|
@ -24150,6 +24151,13 @@ 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\\/12\\.5 {
|
||||
--tw-drop-shadow-alpha: 12.5%;
|
||||
--tw-drop-shadow-size: drop-shadow(0 1px 1px var(--tw-drop-shadow-color, oklab(0% 0 0 / .125)));
|
||||
--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\\/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)));
|
||||
|
|
@ -26901,6 +26909,7 @@ test('text-shadow', async () => {
|
|||
'text-shadow-[shadow:var(--value)]',
|
||||
|
||||
'text-shadow-sm/25',
|
||||
'text-shadow-sm/12.5',
|
||||
'text-shadow-[12px_12px_#0088cc]/25',
|
||||
'text-shadow-[12px_12px_var(--value)]/25',
|
||||
'text-shadow-[10px_10px]/25',
|
||||
|
|
@ -26979,6 +26988,11 @@ test('text-shadow', async () => {
|
|||
text-shadow: 12px 12px var(--tw-text-shadow-color, oklab(59.9824% -.067 -.124 / .25));
|
||||
}
|
||||
|
||||
.text-shadow-sm\\/12\\.5 {
|
||||
--tw-text-shadow-alpha: 12.5%;
|
||||
text-shadow: 0px 1px 2px var(--tw-text-shadow-color, oklab(0% 0 0 / .125)), 0px 2px 2px var(--tw-text-shadow-color, oklab(0% 0 0 / .125));
|
||||
}
|
||||
|
||||
.text-shadow-sm\\/25 {
|
||||
--tw-text-shadow-alpha: 25%;
|
||||
text-shadow: 0px 1px 2px var(--tw-text-shadow-color, oklab(0% 0 0 / .25)), 0px 2px 2px var(--tw-text-shadow-color, oklab(0% 0 0 / .25));
|
||||
|
|
@ -27266,6 +27280,7 @@ test('shadow', async () => {
|
|||
'shadow-[shadow:var(--value)]',
|
||||
|
||||
'shadow-sm/25',
|
||||
'shadow-sm/12.5',
|
||||
'shadow-[12px_12px_#0088cc]/25',
|
||||
'shadow-[12px_12px_var(--value)]/25',
|
||||
'shadow-[10px_10px]/25',
|
||||
|
|
@ -27368,6 +27383,12 @@ test('shadow', async () => {
|
|||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.shadow-sm\\/12\\.5 {
|
||||
--tw-shadow-alpha: 12.5%;
|
||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, oklab(0% 0 0 / .125)), 0 1px 2px -1px var(--tw-shadow-color, oklab(0% 0 0 / .125));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.shadow-sm\\/25 {
|
||||
--tw-shadow-alpha: 25%;
|
||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, oklab(0% 0 0 / .25)), 0 1px 2px -1px var(--tw-shadow-color, oklab(0% 0 0 / .25));
|
||||
|
|
@ -27742,6 +27763,7 @@ test('inset-shadow', async () => {
|
|||
'inset-shadow-[12px_12px_#0088cc,12px_12px_var(--value,#0088cc)]',
|
||||
|
||||
'inset-shadow-sm/25',
|
||||
'inset-shadow-sm/12.5',
|
||||
'inset-shadow-[12px_12px_#0088cc]/25',
|
||||
'inset-shadow-[12px_12px_var(--value)]/25',
|
||||
'inset-shadow-[10px_10px]/25',
|
||||
|
|
@ -27857,6 +27879,12 @@ test('inset-shadow', async () => {
|
|||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.inset-shadow-sm\\/12\\.5 {
|
||||
--tw-inset-shadow-alpha: 12.5%;
|
||||
--tw-inset-shadow: inset 0 1px 1px var(--tw-inset-shadow-color, oklab(0% 0 0 / .125));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.inset-shadow-sm\\/25 {
|
||||
--tw-inset-shadow-alpha: 25%;
|
||||
--tw-inset-shadow: inset 0 1px 1px var(--tw-inset-shadow-color, oklab(0% 0 0 / .25));
|
||||
|
|
|
|||
|
|
@ -4618,7 +4618,7 @@ export function createUtilities(theme: Theme) {
|
|||
if (candidate.modifier.kind === 'arbitrary') {
|
||||
alpha = candidate.modifier.value
|
||||
} else {
|
||||
if (isPositiveInteger(candidate.modifier.value)) {
|
||||
if (isValidOpacityValue(candidate.modifier.value)) {
|
||||
alpha = `${candidate.modifier.value}%`
|
||||
}
|
||||
}
|
||||
|
|
@ -5427,7 +5427,7 @@ export function createUtilities(theme: Theme) {
|
|||
if (candidate.modifier.kind === 'arbitrary') {
|
||||
alpha = candidate.modifier.value
|
||||
} else {
|
||||
if (isPositiveInteger(candidate.modifier.value)) {
|
||||
if (isValidOpacityValue(candidate.modifier.value)) {
|
||||
alpha = `${candidate.modifier.value}%`
|
||||
}
|
||||
}
|
||||
|
|
@ -5573,7 +5573,7 @@ export function createUtilities(theme: Theme) {
|
|||
if (candidate.modifier.kind === 'arbitrary') {
|
||||
alpha = candidate.modifier.value
|
||||
} else {
|
||||
if (isPositiveInteger(candidate.modifier.value)) {
|
||||
if (isValidOpacityValue(candidate.modifier.value)) {
|
||||
alpha = `${candidate.modifier.value}%`
|
||||
}
|
||||
}
|
||||
|
|
@ -5699,7 +5699,7 @@ export function createUtilities(theme: Theme) {
|
|||
if (candidate.modifier.kind === 'arbitrary') {
|
||||
alpha = candidate.modifier.value
|
||||
} else {
|
||||
if (isPositiveInteger(candidate.modifier.value)) {
|
||||
if (isValidOpacityValue(candidate.modifier.value)) {
|
||||
alpha = `${candidate.modifier.value}%`
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue