CSS Specificity Wars in Shopify Stores: 53 Audits with the data Based on an audit of 53 Shopify stores, CSS specificity conflicts were the root cause of roughly 70% of critical app-theme conflicts, with 31 stores containing `!important` declarations injected by apps. The data revealed that 61% of stores using `!important` experienced new layout regressions, and 23 stores had z-index values exceeding 10,000 due to conflicting app injections. The article concludes that the problem is not specificity itself, but developers' escalation to `!important` and arbitrarily high z-index values, which create compounding, unpredictable conflicts. Our first scan of 53 Shopify stores surfaced 147+ critical app-theme conflicts. CSS specificity was the root cause in roughly 70% of them — but specificity itself isn't the problem. The way developers escalate to fix it is. Here's what the data actually shows. The Numbers From our 53-store dataset: 31 stores had important declarations injected by apps 19 of those 31 61% had new layout regressions introduced as a direct result 23 stores had z-index values exceeding 10,000 from conflicting app injections 5 stores had z-index values above 90,000 Top-scoring stores 92+ : 0 important declarations, CSS specificity under 0-2-0, scoped to data attributes Bottom-scoring stores under 75 : 4+ apps, 6+ important rules, specificity above 1-3-0 The stores that scored 92+ weren't running fewer apps by accident. Their apps were built in a way that didn't create specificity conflicts in the first place. Why Specificity Cascades in Shopify Shopify themes are aggressive with specificity. Not maliciously — they have to be, because they own the entire page and need their styles to reliably win against anything merchants might add via the admin. A real example from our dataset. The theme has this targeting a product page review widget: shopify-section-template--product .product block .product description .review-widget { padding: 0; font-size: inherit; } Specificity: 1-3-1 — one ID, three classes, one element. Now an app tries to style that same widget with: .review-widget { padding: 16px; font-size: 14px; } Specificity: 0-1-0. The theme always wins. The review widget renders as a compressed, unreadable block. The app developer sees it, doesn't know the theme's specificity, and reaches for a fix. The important Escalation Trap The first escalation most app developers make is important: .review-widget { padding: 16px important; font-size: 14px important; } This "fixes" the review widget. It also creates a cascade problem that the developer doesn't see until a merchant files a support ticket three weeks later. Here's what happens: a merchant has a custom Liquid snippet that modifies .product-card .review-widget for a specific collection layout. Now they have this in their theme: .product-card .review-widget { padding: 4px; } The theme's base .review-widget rule — now carrying important — overrides even this. The product card layout breaks. The merchant doesn't know which app did it, so they file tickets with all of them. The developer has no idea their important caused it. We found this exact pattern — important from one app breaking a theme customization from a different app — in 19 of the 53 stores we scanned. The important trap is compounding: every new important rule you add to fix your widget creates a risk that it breaks something else, somewhere, in a way you can't predict from your test store. The Z-Index Arms Race The second specificity problem we documented is z-index stacking. Apps that inject fixed-position UI elements popups, notification bars, chat widgets, announcement banners need to win the z-index war to render above the header and other fixed elements. Our dataset showed a predictable arms race: / Theme: standard header / .header wrapper { position: sticky; z-index: 999; } / App A: announcement bar / .announcement-bar { position: fixed; z-index: 1000; / beats header / } / App B: chat widget / .chat-bubble { position: fixed; z-index: 9999; / beats announcement / } / App C: email signup popup / .signup-overlay { position: fixed; z-index: 90000; / beats everything / } Four apps, four different z-index strategies, zero coordination. We found z-index values above 10,000 in 23 of 53 stores. Five stores had values above 90,000. The absurdity of a z-index of 90,000 is a symptom of the problem: there is no standard, so app developers pick arbitrarily large numbers. The fix isn't just picking a bigger number. It's understanding that z-index stacking only matters within the same stacking context — and in Shopify, each position: fixed element creates its own stacking context. Two position: fixed elements at z-index 1000 and 9999 aren't competing on the same axis unless they're in the same viewport layer. A popup overlay and a header aren't actually in the same z-index race unless they've been placed in a shared stacking context by the theme. For app developers: if your popup needs to sit above the header, coordinate with the theme's header z-index directly via a CSS custom property, not an arbitrary large number. What Works: Scoped Selectors The core principle that separates high-scoring stores from low-scoring ones: app CSS should not compete in the theme's specificity range. The cleanest pattern is a data-attribute scoped container. Instead of: / WRONG: competes with theme class selectors / .review-widget { ... } Do this: / RIGHT: container adds specificity, content is predictable / data-app-reviews .review-widget { padding: 16px; font-size: 14px; } The container — data-app-reviews on the element you inject — adds 0-1-0 specificity to every rule. Your inner selectors never need to compete with the theme's 1-2-1 or 1-3-1 chains. You scope yourself, so you don't need important. data-app-reviews .rw-header { ... } data-app-reviews .rw-body { ... } data-app-reviews .rw-footer { ... } Every selector in your stylesheet stays at 0-2-0 or below. No conflict, no escalation. What Works: CSS Layers For browsers that support it which includes the browsers Shopify's audience uses — the store data skews current , @layer gives you a more explicit solution: @layer app-reviews { .review-widget { padding: 16px; font-size: 14px; } } Layers resolve conflicts by layer order, not specificity. If the theme doesn't use layers, your layer's styles will resolve against the unlayered theme cascade. The theme's unlayered selectors still win by default — you don't need important to negotiate that. The advantage over data-attribute scoping: @layer lets you define an explicit priority relationship, so if you need your styles to intentionally sit above certain theme rules, you can declare it rather than brute-forcing it with important. What Works: App Blocks Best Option The most conflict-proof approach is Shopify's app blocks extension — not injecting CSS at all. {% comment %} sections/app-reviews.liquid {% endcomment %} {% schema %} { "name": "Product Reviews", "target": "section" } {% endschema %}