bg-conic not working in next.js production builds
What version of Tailwind CSS are you using? v4.1.6
What build tool (or framework if it abstracts the build tool) are you using? Next.js 15.3.2
What version of Node.js are you using? v22.11.0
What browser are you using? Chrome
What operating system are you using? macOS
Reproduction URL
https://github.com/Multiply/next.js-tailwind-conic-bug
(based on npx create-next-app@latest for the initial commit)
Describe your issue
In next.js production builds (npm run build -> npm run start) bg-conic's background-image does not work.
It works fine in development builds npm run dev.
My initial thoughts would be that it's a @layer issue, but I haven't played too much with it yet, to really debug it further.
Debugged the difference with @tailwindcss/cli --minify. The key difference stopping it from working is:
+++ Next.js
--- @tailwindcss/cli
@property --tw-gradient-from-position {
syntax:"<length-percentage>";
inherits:false;
+ initial-value:0
- initial-value:0%
}
It seems like some extra CSS minifier strips the percentage sign from the above @property value, causing the gradient to stop working.
You can workaround this by adding the class name from-0% but probably not ideal.
- initial-value:0
- initial-value:0% It seems like some extra CSS minifier strips the percentage sign from the above
@propertyvalue, causing the gradient to stop working.
I noticed this initially, and assumed 0 was an accepted value for syntax: "<length-percentage>";
It seems like it does this for other values as well.
Hey!
This is happening because of a minifier used in Next.js. As a workaround, you could add this workaround to your next.config.js file:
const nextConfig = {
webpack(config) {
// Disable CSS minification
config.optimization.minimizer = config.optimization.minimizer.filter((fn: any) => {
return !fn.toString().includes("CssMinimizerPlugin");
});
return config;
},
}
module.exports = nextConfig
Or use ESM / TypeScript, the important part is the webpack config to remove the CSS minifier.
Are there any tickets tracking this issue that you know of, @RobinMalfait?
@Multiply not yet, I have to create a minimal reproduction and open an issue, but wanted to give you a workaround in the meantime.
Following some breadcrumbs, this might have fixed it? https://github.com/cssnano/cssnano/pull/1695
Edit: Seems like nextjs@canary is using [email protected] that doesn't contain the above PR, so maybe upgrading to 7.0.7 could do something.
That might fix it, but I also think that Next.js is bundling the dependency instead of relying on it as an actual dependency. They also use cssnano-simple: https://github.com/vercel/next.js/tree/canary/packages/next/src/bundles/cssnano-simple
That might fix it, but I also think that Next.js is bundling the dependency instead of relying on it as an actual dependency. They also use
cssnano-simple: https://github.com/vercel/next.js/tree/canary/packages/next/src/bundles/cssnano-simple
Yes, that's where I got the above package from, as they include that in there.
As a follow-up, I'm confident the PR I referenced earlier does not fix the underlying issue. It's only about <percentage> and quotes around it, it doesn't affect <length-percentage>. (See my comment here: https://github.com/cssnano/cssnano/pull/1695#issuecomment-2872599305)
I'd assume if we just add one more condition there, it'll be solved.
Edit: I've added a PR to solve it here: https://github.com/cssnano/cssnano/pull/1702
Opened an upstream issue with a reproduction:
- Issue: https://github.com/vercel/next.js/issues/79149
- Reproduction: https://github.com/RobinMalfait/nextjs-incorrect-css-minification
Hi,
The primary step is to meticulously compare the generated CSS output for bg-conic in both dev (inspect element styles) and prod (examining the bundled .css files). Look for:
// (Highly simplified pseudo-code)
interface ThemeConfig { theme: { [scale: string]: { [key: string]: string; // e.g., aspectRatio: { video: '16/9' } }; // ... other theme sections }; // ... other config }
// Existing logic (simplified) for arbitrary values like aspect-[16/9] function migrateArbitraryValue(className: string, themeConfig: ThemeConfig): string | null { const match = className.match(/^([a-z-]+)-[(.+)]$/); // e.g., "aspect-[16/9]" if (match) { const utility = match[1]; // "aspect" const arbitraryValue = match[2]; // "16/9" const themeScale = getThemeScaleForUtility(utility, themeConfig); // e.g., themeConfig.theme.aspectRatio
if (themeScale) {
for (const [key, value] of Object.entries(themeScale)) {
if (value === arbitraryValue) {
return `${utility}-${key}`; // "aspect-video"
}
}
}
} return null; }
// --- NEW/MODIFIED LOGIC INTRODUCED BY THIS PR --- function migrateBareValue(className: string, themeConfig: ThemeConfig): string | null { // 1. Identify if it's a utility that could have a bare value corresponding to a theme value. // This requires parsing the className, e.g., "aspect-16/9" -> utility="aspect", bareValue="16/9" // It needs a robust way to distinguish "16/9" from a named key like "video". // This might involve checking if "16/9" exists as a key in theme.aspectRatio. If not, it's a bare value.
const parsed = parseBareUtility(className); // Helper to get { utility: "aspect", value: "16/9" } if (!parsed) return null;
const { utility, bareValue } = parsed; const themeScale = getThemeScaleForUtility(utility, themeConfig); // e.g., themeConfig.theme.aspectRatio
if (themeScale) {
// 2. Perform a "reverse lookup": find if the bareValue exists as a value in the theme scale.
for (const [key, valueInTheme] of Object.entries(themeScale)) {
if (valueInTheme === bareValue) {
// Found a match: e.g., theme.aspectRatio.video is '16/9'
// And our bareValue is '16/9'
// So, replace 'aspect-16/9' with 'aspect-video'
return ${utility}-${key};
}
}
}
return null; // No named equivalent found for this bare value
}
// Main processing function in the upgrade tool function upgradeFileContent(content: string, themeConfig: ThemeConfig): string { let newContent = content; // Find all class attributes/strings // For each class name: // let newClassName = migrateArbitraryValue(className, themeConfig); // if (!newClassName) { // If not handled by arbitrary, try bare value // newClassName = migrateBareValue(className, themeConfig); // } // if (newClassName) { // replace className with newClassName in newContent; // } return newContent; }
// Helper function (conceptual) function getThemeScaleForUtility(utilityName: string, themeConfig: ThemeConfig): object | undefined { // Maps utility prefixes (e.g., "aspect") to theme scales (e.g., themeConfig.theme.aspectRatio) // This mapping would be predefined or derived. if (utilityName === 'aspect') return themeConfig.theme.aspectRatio; // ... other mappings for colors, spacing, etc. return undefined; }
function parseBareUtility(className: string): { utility: string, value: string } | null { // Complex logic to split, e.g., "aspect-16/9" into "aspect" and "16/9", // or "text-red-500" into "text" and "red-500" (if "red-500" is treated as a bare value for colors) // Needs to handle prefixes, potential negative signs, fractions, etc. // For "aspect-16/9", it's relatively simple. const parts = className.split('-'); if (parts.length < 2) return null; // Not enough parts const utility = parts[0]; // The value might be multiple parts joined by '-', e.g. for colors or complex values // For aspect-16/9, the value is "16/9" which contains a slash. // A more robust parser is needed here. const valueCandidate = parts.slice(1).join('-'); // "16/9"
// This is a simplification; actual parsing is more nuanced
// to avoid mistaking `aspect-video` (where `video` is a key) for a bare value.
// The check usually involves seeing if `valueCandidate` is NOT a key in the theme.
return { utility, value: valueCandidate };
}
Good luck!, SUMAN SUHAG
This issue is happening on Nuxt.js production builds as well
Workarounds and Solutions Disable CSS Minification in Next.js You can disable the problematic CSS minifier in your Next.js production build by modifying your next.config.js file:- // next.config.js const nextConfig = { webpack(config) { // Remove the CSS minifier (CssMinimizerPlugin) that breaks conic gradients config.optimization.minimizer = config.optimization.minimizer.filter((fn) => { return !fn.toString().includes("CssMinimizerPlugin"); }); return config; }, }; module.exports = nextConfig; This disables CSS minification, which prevents the percentage sign from being stripped and allows bg-conic to work as expected in production.
This might work.
Manually patch the output CSS file (post-build hook)
If you want to keep minification but fix the bug, you can patch the final output file via a custom Node script after build.
scripts/patch-css.js
import fs from 'fs';
import path from 'path';
const cssDir = './.next/static/css';
fs.readdirSync(cssDir).forEach(file => {
if (file.endsWith('.css')) {
const filePath = path.join(cssDir, file);
let content = fs.readFileSync(filePath, 'utf8');
// Patch initial-value: 0 to 0% only for --tw-gradient-from-position
content = content.replace(
/@property\s+--tw-gradient-from-position\s*\{([^}]*?)initial-value:\s*0([^%])/,
(_, pre, post) => `@property --tw-gradient-from-position {${pre}initial-value: 0%${post}`
);
fs.writeFileSync(filePath, content, 'utf8');
console.log(`✔ Patched ${file}`);
}
});
Run this after build:
"scripts": {
"build": "next build && node scripts/patch-css.js"
}
✅ This ensures the CSS is fixed after all minification.