From 2683903b86d695f9c2647a06eb58bd96ade1eaee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B5=AD?= Date: Fri, 3 Jul 2026 21:34:02 +0900 Subject: [PATCH] Support fractional opacity modifiers for named shadow sizes (#20302) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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-*/` 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 --- CHANGELOG.md | 1 + packages/tailwindcss/src/utilities.test.ts | 28 ++++++++++++++++++++++ packages/tailwindcss/src/utilities.ts | 8 +++---- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4058a772..4b4256ce9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index e404a0a90..5201878f2 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -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)); diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index c9fb0cc43..5da9e63e1 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -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}%` } }