Skip to content

Commit c5b84d8

Browse files
committed
fix: preserve opacity values in multiple shadows with color-mix
1 parent f302fce commit c5b84d8

1 file changed

Lines changed: 90 additions & 1 deletion

File tree

packages/tailwindcss/src/utils/replace-shadow-colors.ts

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,77 @@ import { segment } from './segment'
33
const KEYWORDS = new Set(['inset', 'inherit', 'initial', 'revert', 'unset'])
44
const LENGTH = /^-?(\d+|\.\d+)(.*?)$/g
55

6+
/**
7+
* Extract the alpha channel from a color value.
8+
* Returns the alpha as a percentage string (e.g., "12%") or null if no alpha is found.
9+
*/
10+
function extractAlpha(color: string): string | null {
11+
// Modern rgba/hsla syntax with slash: rgba(0 0 0 / 0.12) or rgba(0 0 0 / 12%)
12+
const slashAlphaMatch = color.match(/\/\s*([\d.]+)%?\s*\)/)
13+
if (slashAlphaMatch) {
14+
let alpha = slashAlphaMatch[1]
15+
// If it's already a percentage, return it
16+
if (slashAlphaMatch[0].includes('%')) {
17+
return `${alpha}%`
18+
}
19+
// Convert decimal to percentage
20+
const alphaNum = parseFloat(alpha)
21+
if (!isNaN(alphaNum)) {
22+
// Round to avoid floating point precision issues
23+
return `${Math.round(alphaNum * 100)}%`
24+
}
25+
return null
26+
}
27+
28+
// Legacy rgba/hsla syntax with comma: rgba(0, 0, 0, 0.12)
29+
const commaAlphaMatch = color.match(/,\s*([\d.]+)\s*\)/)
30+
if (commaAlphaMatch) {
31+
const alpha = commaAlphaMatch[1]
32+
const alphaNum = parseFloat(alpha)
33+
if (!isNaN(alphaNum)) {
34+
// Round to avoid floating point precision issues
35+
return `${Math.round(alphaNum * 100)}%`
36+
}
37+
return null
38+
}
39+
40+
// No alpha found
41+
return null
42+
}
43+
44+
/**
45+
* Extract the base color without the alpha channel.
46+
* For rgba/hsla, returns rgb/hsl with the same values but without alpha.
47+
* For other colors, returns as-is.
48+
*/
49+
function stripAlpha(color: string): string {
50+
// Modern rgba/hsla syntax with slash: rgba(0 0 0 / 0.12) or hsla(0 0% 0% / 0.3)
51+
const slashMatch = color.match(/^(rgba?|hsla?)\(([\d\s.%]+)\s*\/\s*[\d.]+%?\s*\)$/i)
52+
if (slashMatch) {
53+
const type = slashMatch[1].toLowerCase().replace('a', '')
54+
const values = slashMatch[2].trim()
55+
return `${type}(${values})`
56+
}
57+
58+
// Legacy rgba/hsla syntax with comma: rgba(0, 0, 0, 0.12) or hsla(0, 0%, 0%, 0.3)
59+
const commaMatch = color.match(/^(rgba?|hsla?)\(([\d\s.,%]+),\s*[\d.]+\s*\)$/i)
60+
if (commaMatch) {
61+
const type = commaMatch[1].toLowerCase().replace('a', '')
62+
const values = commaMatch[2].trim()
63+
return `${type}(${values})`
64+
}
65+
66+
// No alpha to strip
67+
return color
68+
}
69+
70+
/**
71+
* Check if a string already contains alpha handling (oklab, color-mix, etc.)
72+
*/
73+
function hasAlphaHandling(value: string): boolean {
74+
return value.includes('oklab(') || value.includes('color-mix(') || value.includes('oklch(')
75+
}
76+
677
export function replaceShadowColors(input: string, replacement: (color: string) => string) {
778
let shadows = segment(input, ',').map((shadow) => {
879
shadow = shadow.trim()
@@ -33,7 +104,25 @@ export function replaceShadowColors(input: string, replacement: (color: string)
33104
// we can't know what to replace.
34105
if (offsetX === null || offsetY === null) return shadow
35106

36-
let replacementColor = replacement(color ?? 'currentcolor')
107+
// Extract alpha from the original color if present
108+
let alpha: string | null = null
109+
let baseColor: string = color ?? 'currentcolor'
110+
111+
if (color) {
112+
alpha = extractAlpha(color)
113+
if (alpha) {
114+
baseColor = stripAlpha(color)
115+
}
116+
}
117+
118+
let replacementColor = replacement(baseColor)
119+
120+
// Only apply color-mix wrapping if:
121+
// 1. The original color had an alpha channel
122+
// 2. The replacement doesn't already have alpha handling (oklab, color-mix, etc.)
123+
if (alpha && !hasAlphaHandling(replacementColor)) {
124+
replacementColor = `color-mix(in srgb, transparent, ${replacementColor} ${alpha})`
125+
}
37126

38127
if (color !== null) {
39128
// If a color was found, replace the color.

0 commit comments

Comments
 (0)