Flash of Unstyled Content (FOUC) is one of those frontend issues that looks small but can completely break the user experience. Recently I had to fix this problem in a Shopify store where the collection filter panel briefly appeared as raw HTML before the page finished loading.
The store was running a paid Shopify theme with several custom edits and third-party apps, so the source of the problem was not obvious.
In this post I’ll show how I tracked down the cause and fixed it without breaking the theme.
The problem
Every time a user opened a collection page, the filter sidebar appeared for a split second without styling. After that, the CSS loaded and everything looked normal.
This happened on:
- Desktop
- Mobile
- Slow connections
- First page load
This is a classic FOUC (Flash of Unstyled Content).
Possible causes
In Shopify themes, FOUC usually happens because of:
- CSS loaded too late
- JavaScript modifying DOM before styles load
- App scripts injected in wrong order
- Deferred or async assets
- Duplicate CSS files
Because this store had custom work from another developer, I suspected an asset loading conflict.
Step 1 — Check theme.liquid
First I opened theme.liquid and checked the order of assets.
I noticed that some CSS files were loaded after scripts, and one app was injecting styles dynamically.
Bad example:
<script src="{{ 'filters.js' | asset_url }}"></script>
<link rel="stylesheet" href="{{ 'theme.css' | asset_url }}">
CSS should load before scripts.
Step 2 — Inspect DevTools
Using Chrome DevTools I watched the Network and Elements tabs while reloading the page.
I saw:
- Filter HTML rendered immediately
- CSS applied later
- App script re-rendered filter
That confirmed the FOUC was caused by render timing.
Step 3 — Temporary hide until ready
Instead of letting the filter render unstyled, I hid it until the page finished loading.
Added class to filter container:
<div class="filter-panel no-fouc">
CSS:
.no-fouc {
visibility: hidden;
}
.js-ready .no-fouc {
visibility: visible;
}
Step 4 — Enable when DOM is ready
Then I added a small script:
document.addEventListener("DOMContentLoaded", function() {
document.documentElement.classList.add("js-ready");
});
This ensures styles are applied before showing UI.
Step 5 — Remove duplicate app assets
I also found one app loading CSS twice.
Removing the duplicate include reduced the flash.
Step 6 — Test in duplicate theme
Important rule when fixing Shopify issues:
Never edit live theme first.
I duplicated the theme, tested there, then published after confirming the fix.
Result
- No more flash
- Filters load clean
- Works on all devices
- No layout shift
Lessons learned
FOUC in Shopify usually comes from asset order, not CSS itself.
Always check:
- theme.liquid
- app includes
- async / defer scripts
- duplicate assets
- DOM render timing
Conclusion
Fixing FOUC is not about hiding problems, it’s about controlling when the UI becomes visible.
Small changes in load order can make a big difference in perceived performance.
Top comments (1)
Good debugging approach, and the
visibility: hidden+js-readyclass pattern is the right fix for DOM-timing FOUC.Worth adding one more layer. The reason this still flashes on slow connections is that all the work — parsing, CSS application, JS execution,
js-readytoggling — happens at click-time. The user waits for DOMContentLoaded to fire before the filter panel appears. On a cold 3G connection that can still be a 1-3 second gap.The complementary approach: move that work before the click. Speculation Rules API (Chrome 108+):
With
moderateeagerness, Chrome prerenders collection pages when a user hovers over or focuses a link. By the time they click, the page is already fully rendered in a background browsing context — CSS applied, JS executed,js-readyclass already added. FOUC doesn't happen because the browser activates an already-complete render rather than initializing one.Your
no-foucfix is still worth keeping (handles direct navigations, bookmarks, hard refreshes), but on any link-click navigation through a Shopify store it becomes a no-op — the page is already done. You can verify withperformance.getEntriesByType('navigation')[0].activationStart > 0on a prerendered load.(I'm the developer of Prefetch — apps.shopify.com/prefetch — which handles the Speculation Rules setup for Shopify stores automatically.)