
Troubleshooting FOUC Issues with Craft CMS on Firefox: A Real User's Journey
You load your Craft CMS site in Firefox, and for a split second, the page looks like it forgot to get dressed. Text sprawls across the screen in Times New Roman, layout boxes stack vertically instead of sitting in their grid, and then—snap—everything fixes itself. That's FOUC (Flash of Unstyled Content), and if you're seeing it only in Firefox while Chrome and Safari behave perfectly, you're dealing with one of the more frustrating browser-specific issues in web development.
This guide walks through why Firefox handles CSS loading differently, how Craft CMS setups (particularly those using Vite or Webpack) can trigger these flashes, and the specific steps to diagnose and fix the problem. We'll cover the common culprits, give you working code examples, and share the debugging approach that actually works.
Prerequisites
Before diving into troubleshooting, make sure you have:
- A Craft CMS 4 or 5 installation where you can modify templates
- Access to your build tool configuration (Vite, Webpack, or Laravel Mix if applicable)
- Firefox Developer Tools familiarity (we'll use Network and Console tabs)
- Basic understanding of how CSS loading affects page rendering
You should also be able to reproduce the FOUC issue consistently. If it only happens occasionally, note whether it occurs on first load, hard refresh, or after clearing cache.
Step 1: Understand Why Firefox Behaves Differently
Firefox's rendering engine starts painting content earlier in the loading process than Chrome. When your CSS isn't available at the moment Firefox decides to render, you get FOUC. Chrome tends to wait a bit longer, which masks timing issues that Firefox exposes.
The console warning you might see—"Layout was forced before the page was fully loaded. If stylesheets are not yet loaded this may cause a flash of unstyled content"—is Firefox telling you exactly what's happening. Your HTML is rendering before CSS is ready.
Three patterns commonly cause this in Craft CMS projects:
CSS loaded through JavaScript: When Vite or Webpack imports CSS in your JavaScript bundle, styles get injected after the script executes. The page renders, then styles apply, creating a visible flash.
Async CSS loading patterns: The rel="preload" with onload swap technique for non-critical CSS can misfire in Firefox, especially if used for your main stylesheet.
Build tool misconfiguration: Development setups that inject styles via Hot Module Replacement sometimes leak into production builds.
Step 2: Inspect Your Current CSS Delivery
Open your Craft site in Firefox with DevTools open. Go to the Network tab, filter by "CSS", and reload the page with cache disabled (hold Shift while clicking reload, or check "Disable cache" in DevTools settings).
Look for these warning signs:
No CSS requests before first paint: If your stylesheet requests start after JavaScript files, or worse, only appear as part of JS bundle requests, Firefox will render unstyled content first.
Only preload links, no stylesheet links: Check your HTML source. If you see this pattern for your main CSS:
<link rel="preload" href="/dist/main.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">Without a fallback <link rel="stylesheet">, Firefox may not apply styles until the JavaScript onload handler fires—which happens after initial render.
CSS imported in JavaScript only: In your Twig layout, if you only see script tags and no stylesheet links, your build tool is probably injecting CSS via JavaScript.
Step 3: Verify Your Twig Template Structure
Open your main layout template (typically templates/_layout.twig or similar). Your <head> section should contain explicit stylesheet links for critical CSS.
Here's what a FOUC-resistant Craft layout looks like:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ siteName }}</title>
{# Critical CSS - blocks rendering until loaded #}
<link rel="stylesheet" href="{{ alias('@web') }}/dist/critical.css">
{# Main stylesheet - also blocking for Firefox reliability #}
<link rel="stylesheet" href="{{ alias('@web') }}/dist/main.css">
{# Preload fonts to prevent layout shifts #}
<link rel="preload" href="{{ alias('@web') }}/fonts/your-font.woff2"
as="font" type="font/woff2" crossorigin>
{% block headStyles %}{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
{# Scripts at end of body #}
<script type="module" src="{{ alias('@web') }}/dist/app.js"></script>
</body>
</html>The key difference from many modern setups: CSS loads via plain <link rel="stylesheet"> tags in <head>, not through JavaScript imports.
Step 4: Fix Build Tool Configuration
If you're using Vite with Craft CMS, your production build needs to extract CSS into separate files rather than bundling them with JavaScript.
For Vite users, check your vite.config.js:
import { defineConfig } from 'vite'
export default defineConfig({
build: {
// Ensure CSS is extracted to separate files
cssCodeSplit: true,
rollupOptions: {
output: {
// Predictable CSS filenames for Craft templates
assetFileNames: (assetInfo) => {
if (assetInfo.name.endsWith('.css')) {
return 'css/[name].[hash].css'
}
return 'assets/[name].[hash][extname]'
}
}
}
},
css: {
// Prevent CSS from being inlined in JS
devSourcemap: true
}
})Then in your Twig templates, link the extracted CSS file directly rather than relying on Vite's JavaScript to inject it:
{% if craft.app.config.general.devMode %}
{# Development: Vite handles CSS via HMR #}
<script type="module" src="http://localhost:5173/@vite/client"></script>
<script type="module" src="http://localhost:5173/src/main.js"></script>
{% else %}
{# Production: Link extracted CSS directly #}
<link rel="stylesheet" href="{{ alias('@web') }}/dist/css/main.css">
<script type="module" src="{{ alias('@web') }}/dist/app.js"></script>
{% endif %}For Webpack users, ensure MiniCssExtractPlugin is configured for production:
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader, // Extracts CSS to files
'css-loader',
'postcss-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash].css'
})
]
}Step 5: Handle Web Fonts Properly
Fonts are a common secondary cause of FOUC. When custom fonts load after initial render, text reflows and layout shifts.
Add font-display: swap to your font declarations:
@font-face {
font-family: 'YourFont';
src: url('/fonts/your-font.woff2') format('woff2');
font-display: swap;
}Preload critical fonts in your Twig layout:
<link rel="preload"
href="{{ alias('@web') }}/fonts/your-font.woff2"
as="font"
type="font/woff2"
crossorigin>The crossorigin attribute is required for font preloading, even for same-origin fonts.
Step 6: Test Your Changes
After making changes, test systematically:
- Hard refresh in Firefox with DevTools open, cache disabled
- Check the Network waterfall: CSS requests should start and complete before or alongside the main document load
- Test with network throttling: In Firefox DevTools, set Network to "Good 3G" and reload—this exaggerates timing issues and makes FOUC more visible if it's still happening
- Test incognito/private mode: Extensions can inject CSS or delay loading; private browsing rules these out
Working with teams has taught us that the most reliable test is opening the site on a fresh Firefox profile with no extensions and no cached data. If FOUC appears there, the problem is in your code, not the user's environment.
Common Mistakes to Avoid
Mistake 1: Using async CSS for your main stylesheet
The preload/onload swap pattern works well for genuinely non-critical CSS (like styles for a modal that appears on user interaction), but using it for your primary layout CSS invites Firefox FOUC.
<!-- Don't do this for main styles -->
<link rel="preload" href="/dist/main.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<!-- Do this instead -->
<link rel="stylesheet" href="/dist/main.css">Mistake 2: Assuming dev server behavior matches production
Vite's dev server uses Hot Module Replacement, which injects CSS via JavaScript. This works fine during development but shouldn't be replicated in production. Always verify your production build extracts CSS to separate files.
Mistake 3: Over-optimizing critical CSS
Critical CSS tools extract above-the-fold styles for inlining. If the extraction is incomplete or the threshold is set too aggressively, users see partially styled content until the full CSS loads. We've found that keeping critical CSS comprehensive (covering all visible layout and typography, not just the bare minimum) prevents this.
Mistake 4: Forgetting about Craft's registerCss()
If you're using Craft modules or plugins that call registerCss() or registerCssFile(), these styles might inject after initial render. Check your modules to ensure they're not adding critical styles this way.
Verification Steps
Once you've implemented fixes, verify with this checklist:
- View page source: Confirm <link rel="stylesheet"> tags appear in <head> before any scripts
- Network tab check: CSS files should have "Initiator" showing as the HTML document, not a JavaScript file
- Multiple Firefox reloads: Do 5-10 hard refreshes; FOUC issues can be intermittent
- Throttled network test: FOUC should not appear even on slow connections
- Console check: Firefox's "Layout was forced" warning should be absent
When Standard Fixes Don't Work
If you've done everything above and still see occasional Firefox FOUC, you're likely hitting a Firefox-specific timing quirk. The community has documented a workaround: adding a tiny inline script at the end of <head>:
<head>
{# ... your meta tags and stylesheets ... #}
<link rel="stylesheet" href="{{ alias('@web') }}/dist/main.css">
{# Firefox FOUC timing fix - place right before </head> #}
<script>let FF_FOUC_FIX;</script>
</head>This empty script declaration appears to influence Firefox's rendering timing enough to prevent the flash. It's not a documented fix, but developers have confirmed it works as recently as late 2024. Our experience shows that this hack becomes unnecessary once CSS delivery is properly structured, but it's a useful diagnostic tool—if adding this script fixes your FOUC, you know the root cause is stylesheet timing rather than something else entirely.
Summary
Firefox FOUC in Craft CMS projects almost always traces back to CSS loading timing: either styles are being injected via JavaScript instead of linked directly, async CSS patterns aren't playing nicely with Firefox's rendering pipeline, or build tools aren't configured to extract CSS for production. The fix involves ensuring render-blocking stylesheet links in your <head>, configuring Vite or Webpack to extract CSS properly, and testing with cache disabled and network throttling to catch any remaining issues.
We've helped several teams work through these exact Firefox styling issues with their Craft CMS builds. If you're still seeing FOUC after working through this guide, or if your setup involves more complex scenarios like multiple CSS bundles or server-side caching layers, we'd be happy to take a look at your specific configuration and help identify what's causing the timing mismatch.
