From b92b9f7bdd3f30e4dac6860c92d3fd329963aea6 Mon Sep 17 00:00:00 2001 From: Ben Maurer Date: Sat, 29 Aug 2026 22:36:41 -0700 Subject: [PATCH] Add a colorMixPolyfill option to @tailwindcss/postcss MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compiler always emits the color-mix() compatibility pair — a plain fallback declaration plus the original gated behind @supports (color: color-mix(in lab, red, red)) — for browsers without color-mix() support (Safari < 16.2). For projects whose browser floor is at or above Tailwind's own supported targets, the gated arm always wins and the fallback arm is dead bytes on the render-blocking path. Measured in a large app: 342 duplicated @supports blocks, ~42KB decoded / ~4KB gzipped, with zero rendering difference on every supported browser. colorMixPolyfill?: boolean (default true, existing behavior unchanged) masks Polyfills.ColorMix out of the compile, mirroring how the plugin already varies the polyfill set per input (Polyfills.All ^ Polyfills.AtProperty for CSS Modules). The compiler-cache key gains the flag so two configs sharing an input file cannot reuse each other's compiler. --- CHANGELOG.md | 1 + .../@tailwindcss-postcss/src/index.test.ts | 27 +++++++++++++++++++ packages/@tailwindcss-postcss/src/index.ts | 17 ++++++++++-- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fec361f5..b39aaafc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add `@tailwindcss/turbopack` package to run Tailwind CSS with Next.js ([20367](https://github.com/tailwindlabs/tailwindcss/pull/20367)) +- Add a `colorMixPolyfill` option to `@tailwindcss/postcss` so projects whose browser targets all support `color-mix()` can drop the duplicated `@supports` fallback declarations ### Fixed diff --git a/packages/@tailwindcss-postcss/src/index.test.ts b/packages/@tailwindcss-postcss/src/index.test.ts index b393cc382..9a8f1931a 100644 --- a/packages/@tailwindcss-postcss/src/index.test.ts +++ b/packages/@tailwindcss-postcss/src/index.test.ts @@ -98,6 +98,33 @@ test('output is optimized by Lightning CSS', async () => { `) }) +test('`color-mix(…)` fallbacks can be disabled', async () => { + let input = css` + @reference 'tailwindcss/theme.css'; + .foo { + @apply text-red-500/50; + } + ` + + // On by default: a `color-mix(…)` declaration is emitted twice — a plain + // fallback, plus the original behind an `@supports` gate. + let withPolyfill = await postcss([ + tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), + ]).process(input, { from: inputCssFilePath() }) + expect(withPolyfill.css).toContain('color-mix(in lab, red, red)') + + // Disabled: the declaration ships once, and nothing else changes. + let withoutPolyfill = await postcss([ + tailwindcss({ + base: `${__dirname}/fixtures/example-project`, + colorMixPolyfill: false, + optimize: { minify: false }, + }), + ]).process(input, { from: inputCssFilePath() }) + expect(withoutPolyfill.css).toContain('color-mix(') + expect(withoutPolyfill.css).not.toContain('@supports') +}) + test('@apply can be used without emitting the theme in the CSS file', async () => { let processor = postcss([ tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index a4a8d9326..05d4e29e6 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -31,7 +31,7 @@ interface CacheEntry { const cache = new QuickLRU({ maxSize: 50 }) function getContextFromCache(postcss: Postcss, inputFile: string, opts: PluginOptions): CacheEntry { - let key = `${inputFile}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}` + let key = `${inputFile}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}:${opts.colorMixPolyfill ?? true}` if (cache.has(key)) return cache.get(key)! let entry = { mtimes: new Map(), @@ -68,12 +68,23 @@ export type PluginOptions = { * Defaults to `true`. */ transformAssetUrls?: boolean + + /** + * Emit `color-mix(…)` fallbacks for browsers without `color-mix()` support + * (Safari < 16.2): a plain fallback declaration, plus a copy of the original + * declaration gated behind `@supports (color: color-mix(in lab, red, red))`. + * + * Defaults to `true`. Set this to `false` when every browser you support has + * `color-mix()`, which removes the duplicated declarations from the output. + */ + colorMixPolyfill?: boolean } function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { let base = opts.base ?? process.cwd() let optimize = opts.optimize ?? process.env.NODE_ENV === 'production' let shouldRewriteUrls = opts.transformAssetUrls ?? true + let colorMixPolyfill = opts.colorMixPolyfill ?? true return { postcssPlugin: '@tailwindcss/postcss', @@ -144,7 +155,9 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { // In CSS Module files, we have to disable the `@property` polyfill since these will // emit global `*` rules which are considered to be non-pure and will cause builds // to fail. - polyfills: isCSSModuleFile ? Polyfills.All ^ Polyfills.AtProperty : Polyfills.All, + polyfills: + (isCSSModuleFile ? Polyfills.All ^ Polyfills.AtProperty : Polyfills.All) & + (colorMixPolyfill ? Polyfills.All : ~Polyfills.ColorMix), }) DEBUG && I.end('Create compiler')