/** * Tailwind 4 gracefully falls back from color-mix() to the base semantic * color, but that turns utilities such as bg-primary/5 into an opaque fill on * Chrome 109. Replace that fallback with an sRGB-channel based alpha color. * The modern color-mix() declaration remains in its @supports block. */ const LEGACY_RGB_TOKENS = new Set([ "accent", "background", "border", "brand-200", "card", "destructive", "foreground", "input", "muted", "muted-foreground", "primary", "primary-foreground", "ring", "secondary", "sidebar-accent", "sidebar-border", "sidebar-foreground", "workspace-brand", ]); const TRANSPARENT_MIX_RE = /color-mix\(\s*in\s+[\w-]+\s*,\s*var\(--([\w-]+)\)\s+([\d.]+)%\s*,\s*transparent\s*\)/i; function legacyAlphaColor(value) { const match = value.match(TRANSPARENT_MIX_RE); if (!match || !LEGACY_RGB_TOKENS.has(match[1])) return null; const [, token, percentage] = match; return `rgb(var(--legacy-${token}-rgb) / ${percentage}%)`; } export default function legacyColorFallback() { return { postcssPlugin: "postcss-legacy-color-fallback", OnceExit(root) { root.walkAtRules("supports", (atRule) => { if (!atRule.params.includes("color-mix")) return; atRule.walkDecls((modernDeclaration) => { const fallbackValue = legacyAlphaColor(modernDeclaration.value); if (!fallbackValue) return; // Tailwind emits the old-browser declaration immediately before // the nested @supports block, in the same utility/variant rule. let fallbackDeclaration = atRule.prev(); while (fallbackDeclaration?.type === "comment") { fallbackDeclaration = fallbackDeclaration.prev(); } if ( fallbackDeclaration?.type === "decl" && fallbackDeclaration.prop === modernDeclaration.prop ) { fallbackDeclaration.value = fallbackValue; } }); }); }, }; } legacyColorFallback.postcss = true;