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:
한국 2026-07-03 21:34:02 +09:00 • committed by GitHub
parent 04588b1e8f
commit 2683903b86
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 33 additions and 4 deletions

View file

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

View file

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

View file

@ -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}%`
}
}