Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Canonicalization: don't crash when plugin utilities throw for unsupported values ([#20052](https://github.com/tailwindlabs/tailwindcss/pull/20052))
- Allow `@apply` to be used with CSS mixins ([#19427](https://github.com/tailwindlabs/tailwindcss/pull/19427))
- Ensure `not-*` correctly negates `@container` queries, including `style(…)` queries ([#20059](https://github.com/tailwindlabs/tailwindcss/pull/20059))
- Ensure shadows with math functions parsed correctly ([#20067](https://github.com/tailwindlabs/tailwindcss/pull/20067))

## [4.3.0] - 2026-05-08

Expand Down
25 changes: 25 additions & 0 deletions packages/tailwindcss/src/utils/replace-shadow-colors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,26 @@ describe('without replacer', () => {
expect(parsed).toMatchInlineSnapshot(`"0 0 0 var(--tw-shadow-color, var(--my-color))"`)
})

it('should handle var color with exotic zero offsets (1)', () => {
let parsed = replaceShadowColors('-0 0e9 0.00 var(--my-color)', replacer)
expect(parsed).toMatchInlineSnapshot(`"-0 0e9 0.00 var(--tw-shadow-color, var(--my-color))"`)
})

it('should handle var color with exotic zero offsets (2)', () => {
let parsed = replaceShadowColors('-0.00 0e-20 0.04px var(--my-color)', replacer)
expect(parsed).toMatchInlineSnapshot(`"-0.00 0e-20 0.04px var(--tw-shadow-color, var(--my-color))"`)
})

it('should handle zeros in colors (1)', () => {
let parsed = replaceShadowColors('1px 2px #000', replacer)
expect(parsed).toMatchInlineSnapshot(`"1px 2px var(--tw-shadow-color, #000)"`)
})

it('should handle zeros in colors (2)', () => {
let parsed = replaceShadowColors('1px 2px rgb(0 0 0)', replacer)
expect(parsed).toMatchInlineSnapshot(`"1px 2px var(--tw-shadow-color, rgb(0 0 0))"`)
})

it('should handle two values with currentcolor', () => {
let parsed = replaceShadowColors('1px 2px', replacer)
expect(parsed).toMatchInlineSnapshot(`"1px 2px var(--tw-shadow-color, currentcolor)"`)
Expand All @@ -49,6 +69,11 @@ describe('without replacer', () => {
`"var(--my-shadow), 1px 1px var(--tw-shadow-color, var(--my-color)), 0 0 1px var(--tw-shadow-color, var(--my-color))"`,
)
})

it('should handle calc()', () => {
let parsed = replaceShadowColors('0 0 calc(1 * var(--spacing)) black', replacer)
expect(parsed).toMatchInlineSnapshot(`"0 0 calc(1 * var(--spacing)) var(--tw-shadow-color, black)"`)
})
})

describe('with replacer', () => {
Expand Down
8 changes: 3 additions & 5 deletions packages/tailwindcss/src/utils/replace-shadow-colors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { isLength } from './infer-data-type'
import { segment } from './segment'

const KEYWORDS = new Set(['inset', 'inherit', 'initial', 'revert', 'unset'])
const LENGTH = /^-?(\d+|\.\d+)(.*?)$/g
const IS_ZERO = /^-?0*\.?0+(?:[eE][+-]?\d+)?$/

export function replaceShadowColors(input: string, replacement: (color: string) => string) {
let shadows = segment(input, ',').map((shadow) => {
Expand All @@ -14,15 +15,12 @@ export function replaceShadowColors(input: string, replacement: (color: string)
for (let part of parts) {
if (KEYWORDS.has(part)) {
continue
} else if (LENGTH.test(part)) {
} else if (isLength(part) || IS_ZERO.test(part)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 isLength misclassifies color functions containing math calls as lengths

isLength delegates to hasMathFn, which returns true for any string that contains a math function call — not only strings that are a math function. Because segment is paren-aware, a color like oklch(calc(50%) 0.1 90deg) arrives as a single token; hasMathFn sees the embedded calc( and returns true, so isLength returns true. With both offsets already filled, the part falls through the length branch silently, color stays null, and the output appends currentcolor instead of wrapping the actual color.

replaceShadowColors('0 0 0 oklch(calc(50%) 0.1 90deg)', replacer) would produce 0 0 0 oklch(calc(50%) 0.1 90deg) var(--tw-shadow-color, currentcolor) rather than the expected 0 0 0 var(--tw-shadow-color, oklch(calc(50%) 0.1 90deg)).

The original LENGTH regex required values to start with a digit, so color function names (which start with letters) could never match. The fix needs to ensure that math-function detection only fires when the math function is at the start of the token, not embedded inside another function.

if (offsetX === null) {
offsetX = part
} else if (offsetY === null) {
offsetY = part
}

// Reset index, since the regex is stateful.
LENGTH.lastIndex = 0
} else if (color === null) {
color = part
}
Expand Down