<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jeff Thomas</title>
    <description>The latest articles on DEV Community by Jeff Thomas (@jthomas2).</description>
    <link>https://dev.to/jthomas2</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3938168%2Fcf3b9560-7ccb-48c9-a960-3e2e7da805a4.png</url>
      <title>DEV Community: Jeff Thomas</title>
      <link>https://dev.to/jthomas2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jthomas2"/>
    <language>en</language>
    <item>
      <title>Using PreFlight to Debug Your Shopify App's Storefront Issues</title>
      <dc:creator>Jeff Thomas</dc:creator>
      <pubDate>Mon, 01 Jun 2026 12:17:51 +0000</pubDate>
      <link>https://dev.to/jthomas2/using-preflight-to-debug-your-shopify-apps-storefront-issues-48nc</link>
      <guid>https://dev.to/jthomas2/using-preflight-to-debug-your-shopify-apps-storefront-issues-48nc</guid>
      <description>&lt;p&gt;You're staring at 14 support tickets before your second coffee.&lt;/p&gt;

&lt;p&gt;"Your app broke my store."&lt;br&gt;
"Checkout button disappeared after your update."&lt;br&gt;
"The cart popup is under the nav now."&lt;/p&gt;

&lt;p&gt;Every one of these tickets means opening Chrome DevTools, navigating to the merchant's store, hunting through layers of CSS, and trying to figure out if &lt;em&gt;your&lt;/em&gt; app actually caused it or if some other app installed three days ago is the real culprit.&lt;/p&gt;

&lt;p&gt;That's the life of a Shopify app developer or app owner. Your app injects UI into hundreds or thousands of storefronts, each one a different combination of theme, apps, and custom code. When something breaks, the merchant blames the most recent change — which is usually you.&lt;/p&gt;

&lt;p&gt;PreFlight exists to change this workflow.&lt;/p&gt;
&lt;h2&gt;
  
  
  What PreFlight Actually Does
&lt;/h2&gt;

&lt;p&gt;PreFlight is a diagnostic engine that scans a merchant's live Shopify storefront and identifies frontend conflicts. Not a staging environment — the live page, exactly as the merchant's customers see it.&lt;/p&gt;

&lt;p&gt;It checks for five categories of issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Z-index stacking conflicts&lt;/strong&gt; — your modal is behind the theme's header because both are &lt;code&gt;z-index: 999&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS specificity collisions&lt;/strong&gt; — the theme's &lt;code&gt;.product-form button&lt;/code&gt; selector is overriding your app's button styles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overflow and clipping&lt;/strong&gt; — your widget is getting cut off by a parent element with &lt;code&gt;overflow: hidden&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visibility rule conflicts&lt;/strong&gt; — something is setting &lt;code&gt;display: none&lt;/code&gt; or &lt;code&gt;opacity: 0&lt;/code&gt; on elements you expect to be visible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party app interference&lt;/strong&gt; — another installed app is modifying DOM elements your app depends on&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key distinction: PreFlight doesn't need access to the theme code or merchant cooperation. It scans the rendered page — the actual DOM, computed styles, and JavaScript execution environment your app is running in.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Workflow: Ticket to Diagnosis in 60 Seconds
&lt;/h2&gt;

&lt;p&gt;Here's how it actually works in practice.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1: Extract the store URL from the ticket
&lt;/h3&gt;

&lt;p&gt;You get a support ticket. The merchant's store is &lt;code&gt;merchant-store.myshopify.com&lt;/code&gt;. That's all you need.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: Run the PreFlight scan
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example API request (illustrative — full API surface not yet public)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.preflight.technology/v1/scan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PREFLIGHT_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;store_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;merchant-store.myshopify.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;app_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-app-name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;app_name&lt;/code&gt; parameter is important. It tells PreFlight's detection engine which DOM elements, scripts, and stylesheets belong to your app versus the theme versus other installed apps.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Read the diagnostic report
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scan_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scan_8f3a92c1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"store_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"merchant-store.myshopify.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total_issues"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"critical"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"warnings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"your_app_caused"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issues"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"z_index_conflict"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"critical"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"element"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".your-app-modal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"conflicting_element"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".theme-header"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"your_app_z_index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"conflicting_z_index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"theme"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"suggested_fix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Increase z-index on .your-app-modal to 1001 or higher"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Notice &lt;code&gt;summary.your_app_caused: 0&lt;/code&gt;. That field is the one that matters most.&lt;/p&gt;
&lt;h2&gt;
  
  
  The 'Not Our Fault' Field
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;summary.your_app_caused&lt;/code&gt; tells you how many of the detected issues are attributable to your app's code versus the theme or other apps.&lt;/p&gt;

&lt;p&gt;When it's zero, you have evidence. Not a hunch — evidence. You can reply to the merchant:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We ran a diagnostic scan of your store. The conflict is caused by your theme's z-index configuration, not our app. Here's the report showing the specific elements involved."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a conversation that used to take 30 minutes of manual investigation. Now it takes 60 seconds.&lt;/p&gt;

&lt;p&gt;When it's non-zero, you know exactly what to fix before you even open an editor.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why This Matters for App Owners (Not Just Developers)
&lt;/h2&gt;

&lt;p&gt;The numbers tell the real story.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workflow&lt;/th&gt;
&lt;th&gt;Time per ticket&lt;/th&gt;
&lt;th&gt;Monthly cost (50 tickets)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Manual DevTools investigation&lt;/td&gt;
&lt;td&gt;15-30 min&lt;/td&gt;
&lt;td&gt;12-25 hrs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PreFlight scan + report&lt;/td&gt;
&lt;td&gt;30-60 sec&lt;/td&gt;
&lt;td&gt;&amp;lt; 1 hr&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That 8 hours/month reclaimed is illustrative — your actual time savings depend on ticket volume, store complexity, and your team's debugging speed. But the directional math is real.&lt;/p&gt;

&lt;p&gt;The bigger unlock is &lt;strong&gt;proactive monitoring&lt;/strong&gt;. Instead of waiting for merchants to file tickets, you can scan your top 100 merchants periodically. If a theme update introduced a z-index conflict last Tuesday, you find out before the merchant does.&lt;/p&gt;

&lt;p&gt;For app owners running support teams, this shifts the economics. Your support agents open tickets that already have a diagnosis attached. Less senior developer time spent on "is this even our bug" triage.&lt;/p&gt;
&lt;h2&gt;
  
  
  How PreFlight Adapts to Your App
&lt;/h2&gt;

&lt;p&gt;The core scanning engine is universal — it works for any Shopify app. But the detection rules are customer-specific.&lt;/p&gt;

&lt;p&gt;When you onboard with PreFlight, you configure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your DOM selectors&lt;/strong&gt; — which elements belong to your app&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expected behavior rules&lt;/strong&gt; — "this element should always be visible on product pages"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Known conflict patterns&lt;/strong&gt; — z-index ranges your app uses, CSS classes that conflict historically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means PreFlight finds &lt;em&gt;your app's problems&lt;/em&gt; in the context of each merchant's specific storefront.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where PreFlight Fits in Your Stack
&lt;/h2&gt;

&lt;p&gt;The current workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Merchant files ticket -&amp;gt; Support agent reads it -&amp;gt; Dev investigates -&amp;gt; Fix deployed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PreFlight workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ticket arrives -&amp;gt; PreFlight scans -&amp;gt; Diagnostic attached -&amp;gt; Agent opens pre-diagnosed ticket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fully automated version — where every incoming support ticket triggers a scan and the report is attached before a human reads it — is on the roadmap. Right now, PreFlight works best in the "manual trigger" model: get a ticket, run a scan, have an answer in 60 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try PreFlight
&lt;/h2&gt;

&lt;p&gt;If you're shipping Shopify apps and spending real developer hours on storefront conflict investigations, PreFlight is worth testing.&lt;/p&gt;

&lt;p&gt;Early access: &lt;a href="https://preflight.technology/insights?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=article5" rel="noopener noreferrer"&gt;preflight.technology&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Series: PreFlight Store Audits
&lt;/h2&gt;

&lt;p&gt;This is Article #5 in the &lt;strong&gt;preflight-53-store-audit&lt;/strong&gt; series.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/jthomas2/what-we-found-scanning-53-shopify-stores-for-app-theme-conflicts-2h"&gt;Article #1: What We Found Scanning 53 Shopify Stores for App-Theme Conflicts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/jthomas2/css-specificity-wars-in-shopify-stores-53-audits-with-the-data-19d8"&gt;Article #2: CSS Specificity Wars in Shopify Stores: What 53 Audits Revealed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/jthomas2/how-to-debug-shopify-theme-app-extension-conflicts-3m6b"&gt;Article #3: How to Debug Shopify Theme App Extension Conflicts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/jthomas2/a-taxonomy-of-shopify-app-theme-conflicts-147-issues-classified-1ng6"&gt;Article #4: A Taxonomy of Shopify App-Theme Conflicts: 147 Issues Classified&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Article #5: This article&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>shopify</category>
      <category>shopifyapps</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A Taxonomy of Shopify App-Theme Conflicts: 147 Issues Classified</title>
      <dc:creator>Jeff Thomas</dc:creator>
      <pubDate>Fri, 29 May 2026 13:23:40 +0000</pubDate>
      <link>https://dev.to/jthomas2/a-taxonomy-of-shopify-app-theme-conflicts-147-issues-classified-1ng6</link>
      <guid>https://dev.to/jthomas2/a-taxonomy-of-shopify-app-theme-conflicts-147-issues-classified-1ng6</guid>
      <description>&lt;p&gt;Our first three articles documented the damage: 53 stores, 147+ conflicts, CSS specificity cascading in 70% of cases, z-index values exceeding 90,000 in five stores. We've also published two technical guides — one on debugging workflows, one on CSS specificity mechanics.&lt;/p&gt;

&lt;p&gt;This article does something different. It's a classification framework — a taxonomy of the conflict types we found, organized so that when you encounter a new conflict, you can reason about it precisely instead of guessing.&lt;/p&gt;

&lt;p&gt;The taxonomy has five primary categories. Most conflicts map cleanly to one; the hard ones touch two or three simultaneously.&lt;/p&gt;




&lt;h2&gt;
  
  
  Category 1: CSS Specificity Cascades
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prevalence:&lt;/strong&gt; 70% of all conflicts in our 53-store dataset&lt;/p&gt;

&lt;p&gt;This is the most documented conflict type and the most misunderstood. Developers know CSS specificity exists. They don't know how Shopify themes use it — not as a styling preference, but as a structural defense mechanism.&lt;/p&gt;

&lt;p&gt;Shopify themes own the entire page. They need their styles to reliably win against anything merchants might add via the admin. So themes target with high specificity chains: &lt;code&gt;#shopify-section-template--product .product__block .product-reviews&lt;/code&gt; — specificity &lt;code&gt;1-3-1&lt;/code&gt;. An app targeting &lt;code&gt;.product-reviews&lt;/code&gt; has &lt;code&gt;0-1-0&lt;/code&gt;. The theme wins. Every time.&lt;/p&gt;

&lt;p&gt;The escalation most developers reach for is &lt;code&gt;!important&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* "Fixing" the widget */&lt;/span&gt;
&lt;span class="nc"&gt;.product-reviews&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works for the widget. It creates a cascading regression problem that the developer doesn't see until a merchant files a ticket three weeks later. A merchant's custom Liquid snippet targeting &lt;code&gt;.product-card .product-reviews&lt;/code&gt; gets overridden by the app's &lt;code&gt;!important&lt;/code&gt; rule. Layout breaks. Nobody knows which app did it.&lt;/p&gt;

&lt;p&gt;We found this exact pattern — &lt;code&gt;!important&lt;/code&gt; from one app breaking a theme customization from a different source — in &lt;strong&gt;19 of the 53 stores&lt;/strong&gt; we scanned.&lt;/p&gt;

&lt;p&gt;The CSS specificity category includes z-index stacking conflicts, which are a specificity problem in the stacking context dimension. Apps that inject fixed-position UI elements (announcement bars, chat widgets, popups) race to win visual hierarchy. We found z-index values above 10,000 in &lt;strong&gt;23 of 53 stores&lt;/strong&gt;, five above 90,000. There's no standard, so developers pick arbitrarily large numbers, and the escalation has no ceiling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Classification signal:&lt;/strong&gt; A widget renders with wrong spacing, wrong font, or is visually compressed — but no JavaScript errors appear in the console.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Scope your CSS to your container element with a data attribute. Never ship bare class selectors. Keep every rule under &lt;code&gt;0-2-0&lt;/code&gt; specificity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Wrong: bare class, competes with theme */&lt;/span&gt;
&lt;span class="nc"&gt;.review-widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Right: scoped, self-contained specificity */&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-app-preflight&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nc"&gt;.review-widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Category 2: Event Handler Collisions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prevalence:&lt;/strong&gt; ~20% of conflicts; hardest to resolve when multiple apps are involved&lt;/p&gt;

&lt;p&gt;This is the conflict type developers least expect and most struggle to debug. The symptom: a form submit either fires twice, fires the wrong action, or does nothing. No console errors. The DevTools Elements panel shows no obvious problem.&lt;/p&gt;

&lt;p&gt;The root cause: two apps both binding to the same form's submit event, both calling &lt;code&gt;preventDefault()&lt;/code&gt;. The first listener fires and cancels the submission. The second listener fires — but the form is already cancelled. Nothing happens.&lt;/p&gt;

&lt;p&gt;We found this exact pattern in &lt;strong&gt;11 of 53 stores&lt;/strong&gt;. None of the merchants knew why their add-to-cart button was broken. Each app worked in isolation; together, they silently cancelled each other out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// App A: wishlist&lt;/span&gt;
&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;trackWishlistConversion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// App B: upsell — same form, same pattern&lt;/span&gt;
&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;showUpsellModal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Both preventDefault() fires. Form never submits.&lt;/span&gt;
&lt;span class="c1"&gt;// No console error. No visible signal.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Classification signal:&lt;/strong&gt; An action (form submit, button click) fires but doesn't complete. Other features work normally. The issue appears only when multiple apps are installed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix — short version:&lt;/strong&gt; Don't hijack native submit on shared forms. Use a cancelable custom event and check the dispatch result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CustomEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app:cart-before-add&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;cancelable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wasNotCancelled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wasNotCancelled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If another app cancelled the event, your app respects it and exits cleanly. If it wasn't cancelled, you proceed. This is a coordination protocol, not a form hijack.&lt;/p&gt;




&lt;h2&gt;
  
  
  Category 3: DOM Mutation Cascades
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prevalence:&lt;/strong&gt; ~10% of conflicts; often the root cause of performance issues too&lt;/p&gt;

&lt;p&gt;This category has two sub-patterns. The first is brittle injection — apps targeting a specific class selector that changes when the theme updates. Theme version &lt;code&gt;x.y&lt;/code&gt; changes &lt;code&gt;.product__info-container&lt;/code&gt; to &lt;code&gt;.product__info-wrapper&lt;/code&gt;. The app's DOM query returns null. Widget doesn't render. No error — just an empty space where the widget should be.&lt;/p&gt;

&lt;p&gt;We tracked stores across multiple scans. &lt;strong&gt;Theme updates caused at least one app widget to disappear in 8 of 53 stores&lt;/strong&gt; during our observation window.&lt;/p&gt;

&lt;p&gt;The second sub-pattern is the MutationObserver pileup. Some apps use &lt;code&gt;MutationObserver&lt;/code&gt; to watch for DOM changes and re-inject their widgets when the theme updates. Rational approach — except when multiple apps all observe &lt;code&gt;document.body&lt;/code&gt; with &lt;code&gt;subtree: true&lt;/code&gt;. Each injection triggers the other observers. A tight re-render loop emerges.&lt;/p&gt;

&lt;p&gt;In the worst case from our dataset, three apps with &lt;code&gt;MutationObserver&lt;/code&gt; watching &lt;code&gt;document.body&lt;/code&gt; pushed page load time from &lt;strong&gt;2.1s to 6.8s&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Classification signal:&lt;/strong&gt; A widget appears on page load, then disappears after a few seconds. Or page performance degrades progressively as more apps load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Use Shopify's app block architecture instead of DOM injection. App blocks render inside the theme's section pipeline — the theme controls rendering order, and no observer is needed.&lt;/p&gt;

&lt;p&gt;If you must use &lt;code&gt;MutationObserver&lt;/code&gt;, scope it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong: watches entire body subtree&lt;/span&gt;
&lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;childList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;subtree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Right: watches only the container you control&lt;/span&gt;
&lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetContainer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;childList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;subtree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Category 4: Extension API Failures
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prevalence:&lt;/strong&gt; Appears in stores running data-driven apps (reviews, recommendations, loyalty); top pain point on Stack Overflow&lt;/p&gt;

&lt;p&gt;This is the conflict category most invisible to merchants and most frustrating for developers. The app installs correctly. The widget renders. It shows nothing — no data, no error, just a blank widget.&lt;/p&gt;

&lt;p&gt;The failure chain: the Theme App Extension makes a &lt;code&gt;fetch()&lt;/code&gt; call to the app backend. The response returns — but the JavaScript doesn't handle the empty state. A widget renders with no content because &lt;code&gt;data&lt;/code&gt; is an empty object &lt;code&gt;{}&lt;/code&gt;, and no one wrote the fallback.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Silent failure pattern&lt;/span&gt;
&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/apps/your-app/data?productId=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;renderWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// data = {}, renders blank&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// No catch(). No empty-state handler.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second failure mode: developers try to pass Liquid template variables to the extension's JavaScript. It works in the theme context. But the &lt;code&gt;checkout&lt;/code&gt; object isn't populated during section render — it only exists during checkout. Discount logic that works in dev silently fails in production.&lt;/p&gt;

&lt;p&gt;The Stack Overflow question "Passing data between Theme App Extension and backend" has the most votes of any &lt;code&gt;shopify-app&lt;/code&gt; extension question. This is not a niche issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Classification signal:&lt;/strong&gt; Widget renders but shows no data. Network tab shows a 200 response with an empty body. No console error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Use the app proxy endpoint, not Liquid variables, to pass data between extension and backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Extension fetches from app proxy — always gets fresh, correct data&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMContentLoaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-app-preflight]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`/apps/your-app/api/widget-data?productId=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;recommendations&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;renderWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;recommendations&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always handle the empty state explicitly. If &lt;code&gt;data&lt;/code&gt; is empty, render a placeholder or hide the widget cleanly. Never let a widget render blank silently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Category 5: Script Loading Order Conflicts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prevalence:&lt;/strong&gt; Common in stores with legacy app stacks; often the hidden cause of intermittent failures&lt;/p&gt;

&lt;p&gt;Apps that depend on jQuery face a loading order problem: the theme loads jQuery deferred, and the app's inline script executes immediately. jQuery isn't there yet. The script fails — but silently, because there's no error boundary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Theme loads jQuery deferred --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"jquery-3.6.0.js"&lt;/span&gt; &lt;span class="na"&gt;defer&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- App's inline script — runs immediately, jQuery not loaded yet --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* fails silently */&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test store works (jQuery loaded synchronously). Production fails. The merchant sees intermittent failures with no pattern.&lt;/p&gt;

&lt;p&gt;A secondary version: multiple scripts targeting the same initialization point load in unpredictable order. App A's script runs before App B's dependency is ready. App B fails silently. The symptoms don't point to App A at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Classification signal:&lt;/strong&gt; A feature works in incognito mode but fails in a normal browser session. Console shows &lt;code&gt;"X is not defined"&lt;/code&gt;. Failures are intermittent and don't reproduce consistently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Replace inline scripts with &lt;code&gt;defer&lt;/code&gt; to guarantee execution order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;defer&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"{{ 'app-reviews.js' | asset_url }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app-reviews.js — runs after DOM is ready because of defer&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMContentLoaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.review-widget&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleReviewClick&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop jQuery entirely. Vanilla JS event listeners eliminate the entire dependency chain.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Conflict Severity Matrix
&lt;/h2&gt;

&lt;p&gt;Not all conflicts are equal to fix. Based on our 53-store data:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Frequency&lt;/th&gt;
&lt;th&gt;Fixability&lt;/th&gt;
&lt;th&gt;Primary Fixer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CSS Specificity&lt;/td&gt;
&lt;td&gt;Very High&lt;/td&gt;
&lt;td&gt;Easy&lt;/td&gt;
&lt;td&gt;App developer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event Handler Collisions&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Hard&lt;/td&gt;
&lt;td&gt;App developer (requires coordination)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DOM Mutation Cascades&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;App developer (app blocks)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extension API Failures&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;App developer (architecture)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Script Loading Order&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Easy&lt;/td&gt;
&lt;td&gt;App developer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The hardest category — event handler collisions — requires coordination across apps you may not own. The fix is architectural: don't bind to shared theme elements. Listen on your own injected elements only.&lt;/p&gt;

&lt;p&gt;The stores that scored 92+ in our scans had zero &lt;code&gt;!important&lt;/code&gt; declarations, zero MutationObserver pileups, and were built on Shopify's App Block architecture. None of their conflicts were unsolvable. They were just built in a way that didn't create the conditions for cascading failures.&lt;/p&gt;




&lt;p&gt;We've published the full conflict taxonomy — 147 issues across five categories, with per-store breakdowns by conflict type and app category — at &lt;a href="https://preflight.technology/insights?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=article4" rel="noopener noreferrer"&gt;preflight.technology/insights&lt;/a&gt;. No signup required. The dataset updates as we scan new stores.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with &lt;a href="https://preflight.technology" rel="noopener noreferrer"&gt;PreFlight&lt;/a&gt; — storefront compatibility scanning for Shopify app developers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>shopifyapps</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Debug Shopify Theme App Extension Conflicts</title>
      <dc:creator>Jeff Thomas</dc:creator>
      <pubDate>Tue, 26 May 2026 12:26:52 +0000</pubDate>
      <link>https://dev.to/jthomas2/how-to-debug-shopify-theme-app-extension-conflicts-3m6b</link>
      <guid>https://dev.to/jthomas2/how-to-debug-shopify-theme-app-extension-conflicts-3m6b</guid>
      <description>&lt;p&gt;Our first two articles established the damage: CSS specificity conflicts caused regressions in 61% of the 53 stores we scanned, and z-index arms races produced values above 90,000 in five of them. But we glossed over the hardest part for developers who are actually in the fight: how do you systematically find and fix these conflicts when you didn't write the theme and you don't know which of your three apps is causing the problem?&lt;/p&gt;

&lt;p&gt;This article is the workflow. It's the checklist we use when debugging a store that has app-theme conflicts — no theory, no patterns-to-know — just the debugging steps in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Isolate the Problem to One App
&lt;/h2&gt;

&lt;p&gt;Before you fix anything, confirm which app is responsible. Running multiple apps with conflicts produces symptoms that look like a single problem — broken layout, missing widgets, sluggish performance — but they have different causes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The process:&lt;/strong&gt; Disable apps one at a time, starting with the most recently installed, and reload the storefront after each disable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// First pass: find the conflict window
// 1. Disable the most recently installed app
// 2. Reload the storefront, check DevTools Elements panel
// 3. If still broken → re-enable, disable the next most recent
// 4. If fixed → you have your candidate
Most conflicts surface within two disable cycles. If the store has seven apps and none of them individually cause the problem, you have a multi-app cascade — go to Step 3.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What to check in DevTools while isolating:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Open Console, filter by error level
// Look for:
// - "Cannot read property of undefined" (DOM query failed)
// - "Maximum update depth exceeded" (MutationObserver loop)
// - "TypeError: jQuery is not defined" (loading order failure)

// Open Network tab, filter by JS
// Look for:
// - Failed script loads (red row in Network)
// - Scripts loaded in wrong order (check Initiator column)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal of isolation is a single app. If you can't narrow it to one, document every symptom carefully — multiple apps sometimes create a conflict that's only visible when they're all running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Identify the Conflict Type
&lt;/h2&gt;

&lt;p&gt;Once you've isolated the app, classify the conflict. Each type has a different debugging approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Type A: CSS Specificity&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Symptom&lt;/strong&gt;: A widget renders with wrong spacing, wrong font size, or is visually compressed or hidden.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to confirm:&lt;/strong&gt; Inspect the widget in DevTools Elements panel. Look at the CSS cascade in the Styles pane — if the widget's rules are crossed out and replaced by a theme selector, specificity is the issue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// In DevTools Console — check computed specificity of conflicting selectors
const el = document.querySelector('.your-widget-class');
const styles = getComputedStyle(el);
const computedPadding = styles.paddingTop; // if "0px" but you expected "16px", specificity won
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Confirm with specificity:&lt;/strong&gt; Inspect the affected element. Click the CSS selector in the Styles pane. Chrome DevTools shows specificity in a tooltip (e.g., [0, 2, 1]). If the theme's selector is [1, 3, 0] and yours is [0, 1, 0], you lose. Every time.&lt;/p&gt;

&lt;p&gt;The fix from our 53-store data: scope your CSS to a data attribute on your container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/* If your widget is injecting into .product-info */
[data-app-reviews] .review-widget {
  padding: 16px;
  font-size: 14px;
}
/* Not .review-widget alone — that loses to the theme */
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Type B: Event Handler Collision&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Symptom&lt;/strong&gt;: A form submit doesn't fire, fires twice, or fires the wrong action. A button click triggers nothing or triggers multiple actions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to confirm:&lt;/strong&gt; Open DevTools Sources panel, set a breakpoint on the event listener. Or use the Event Listener tab: find the element, expand the click/submit listeners, check which function runs first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// In Console — see all click handlers on a button
const btn = document.querySelector('[name="add"]');
getEventListeners(btn); // Chrome only — lists all listeners with their source file

// Check if multiple listeners call preventDefault()
document.querySelectorAll('form[action="/cart/add"]').forEach(form =&amp;gt; {
  let preventDefaultCount = 0;
  form.addEventListener('submit', (e) =&amp;gt; {
    console.log('submit intercepted');
    // Count how many times this fires per submit
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The root cause:&lt;/strong&gt; Multiple apps binding to the same form or button and all calling preventDefault(). The first listener to fire cancels the submission. Other listeners never execute.&lt;br&gt;
**&lt;br&gt;
The fix: **Don't hijack native submit. Emit a custom event and let other listeners coordinate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Instead of:
form.addEventListener('submit', (e) =&amp;gt; {
  e.preventDefault();
  doYourThing();
  form.submit();
});

// Do this:
form.addEventListener('submit', (e) =&amp;gt; {
  // Let the event bubble but coordinate via custom event
  const event = new CustomEvent('app:cart-before-add', {
    detail: { form, product: window.ShopifyAnalytics.meta.product_id },
    cancelable: true
  });
  const wasNotCancelled = document.dispatchEvent(event);
  if (wasNotCancelled) {
    form.submit();
  }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the form needs to show a modal first (upsell, wishlist confirmation), use cancelable: true and check the dispatch result. If another app cancelled the event, your app respects it. If it wasn't cancelled, you proceed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Type C: DOM Mutation Cascade&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Symptom&lt;/strong&gt;: A widget appears on initial load, then disappears after a few seconds. Or a widget only appears on some pages, not others.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to confirm:&lt;/strong&gt; Open DevTools, go to the Elements panel, right-click the &lt;/p&gt; tag → "Break on" → "Subtree modifications." Reload the page. Every time the DOM changes, the debugger pauses. Walk forward through each pause and note which app's script triggered the change.&lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// In Console — observe mutation events (no debugger needed)
const observer = new MutationObserver((mutations) =&amp;gt; {
  mutations.forEach(m =&amp;gt; {
    if (m.type === 'childList') {
      console.log('DOM change:', m.addedNodes.length, 'nodes added',
        'by:', m.target.closest('[data-app]')?.dataset.app || 'unknown source');
    }
  });
});

observer.observe(document.body, { childList: true, subtree: true });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;The root cause:&lt;/strong&gt; Apps use MutationObserver to detect theme changes and re-inject widgets. When three apps all observe document.body for childList changes, each injection triggers the other observers, and a tight render loop emerges.&lt;/p&gt;

&lt;p&gt;In our dataset, the worst case: a store with three apps all observing document.body pushed page load from 2.1s to 6.8s.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Remove the MutationObserver entirely and use Shopify's app block extension instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{% comment %} Your app block — section/app-reviews.liquid {% endcomment %}
{% schema %}
{
  "name": "Product Reviews",
  "target": "section",
  "settings": [
    { "type": "product" }
  ]
}
{% endschema %}

&amp;lt;div class="review-widget" data-product-id="{{ product.id }}"&amp;gt;
  &amp;lt;!-- Content — no MutationObserver, no DOM queries --&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Merchants place the block in the theme editor. The theme controls rendering order. No observer needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Fix the Conflict
&lt;/h2&gt;

&lt;p&gt;Once isolated and classified, apply the pattern from the relevant section above. But also check these two things that appear in nearly every conflict:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Script loading order&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- If your app depends on jQuery — don't assume it's there --&amp;gt;
&amp;lt;!-- WRONG: inline script that assumes jQuery exists --&amp;gt;
&amp;lt;script&amp;gt;
  $(document).ready(function() { /* fails silently */ });
&amp;lt;/script&amp;gt;

&amp;lt;!-- RIGHT: defer and run after DOM is ready --&amp;gt;
&amp;lt;script defer src="{{ 'app-reviews.js' | asset_url }}"&amp;gt;&amp;lt;/script&amp;gt;

// app-reviews.js
document.addEventListener('DOMContentLoaded', () =&amp;gt; {
  // DOM is ready, no jQuery dependency
  document.querySelector('.review-widget').addEventListener('click', handleClick);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. DOM query brittleness&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// WRONG: assumes specific class exists after theme update
const container = document.querySelector('.product__info-container');
container.insertAdjacentHTML('afterend', widgetHTML);

// RIGHT: use Shopify's established injection points via app block
// Or fallback gracefully if the target doesn't exist
const container = document.querySelector('.product__info-container, .product-info, [data-product-section]');
if (container) {
  container.insertAdjacentHTML('afterend', widgetHTML);
} else {
  console.warn('[AppReviews] Injection target not found — skipping render');
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Checklist (Summary)
&lt;/h2&gt;

&lt;p&gt;Before shipping any app update to production, run through this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Isolate&lt;/strong&gt; — confirm which app causes the conflict, not "which conflict exists"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classify&lt;/strong&gt; — CSS specificity, event collision, or DOM mutation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check specificity&lt;/strong&gt; — DevTools shows specificity in selector tooltip; if your selector is lower, it loses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check event listeners&lt;/strong&gt; — DevTools Elements panel → Event Listeners tab shows every listener on an element&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check script order&lt;/strong&gt; — Network tab, filter by JS; confirm deferred scripts run after synchronous ones&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope CSS&lt;/strong&gt; — data-attribute container, not bare class selectors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use custom events&lt;/strong&gt; — not &lt;code&gt;preventDefault()&lt;/code&gt; hijacking of native form submissions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with 3+ apps installed&lt;/strong&gt; — your app never runs alone in production&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Post-Debug: Prevent the Next One
&lt;/h2&gt;

&lt;p&gt;After fixing the conflict, the question is always "what do we do so this doesn't happen again?" Three things that work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ship scoped CSS from day one.&lt;/strong&gt; Use data-app-[slug] on every injected element. Never ship a bare class selector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with the merchant's actual app stack, not your demo store.&lt;/strong&gt; The conflict almost never reproduces in your test store because it doesn't have their other apps installed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use app blocks for widget injection.&lt;/strong&gt; It shifts the responsibility from "hope the DOM structure is stable" to "the merchant placed it where it should be."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The aggregate conflict dataset — including the 53-store breakdown by conflict type, app category, and theme — is at &lt;a href="https://preflight.technology/insights" rel="noopener noreferrer"&gt;preflight.technology/insights&lt;/a&gt;. You can filter by app category and see how common your specific conflict pattern is across the dataset. No signup required to browse.&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>CSS Specificity Wars in Shopify Stores: 53 Audits with the data</title>
      <dc:creator>Jeff Thomas</dc:creator>
      <pubDate>Fri, 22 May 2026 12:48:44 +0000</pubDate>
      <link>https://dev.to/jthomas2/css-specificity-wars-in-shopify-stores-53-audits-with-the-data-19d8</link>
      <guid>https://dev.to/jthomas2/css-specificity-wars-in-shopify-stores-53-audits-with-the-data-19d8</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Here's what the data actually shows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;p&gt;From our 53-store dataset:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;31 stores&lt;/strong&gt; had !important declarations injected by apps&lt;/p&gt;

&lt;p&gt;**19 of those 31 (61%) **had new layout regressions introduced as a direct result&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;23 stores&lt;/strong&gt; had z-index values exceeding 10,000 from conflicting app injections&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5 stores&lt;/strong&gt; had z-index values above 90,000&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Top-scoring stores (92+)&lt;/strong&gt;: 0 !important declarations, CSS specificity under 0-2-0, scoped to data attributes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bottom-scoring stores (under 75)&lt;/strong&gt;: 4+ apps, 6+ !important rules, specificity above 1-3-0&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Specificity Cascades in Shopify
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;A real example from our dataset. The theme has this targeting a product page review widget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nf"&gt;#shopify-section-template--product&lt;/span&gt;
  &lt;span class="nc"&gt;.product__block&lt;/span&gt;
  &lt;span class="nc"&gt;.product__description&lt;/span&gt;
  &lt;span class="nc"&gt;.review-widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inherit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Specificity: 1-3-1 — one ID, three classes, one element.&lt;/p&gt;

&lt;p&gt;Now an app tries to style that same widget with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.review-widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Specificity: 0-1-0.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The !important Escalation Trap&lt;/p&gt;

&lt;p&gt;The first escalation most app developers make is !important:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.review-widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.product-card&lt;/span&gt; &lt;span class="nc"&gt;.review-widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Z-Index Arms Race
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Our dataset showed a predictable arms race:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Theme: standard header */&lt;/span&gt;
&lt;span class="nc"&gt;.header__wrapper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sticky&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;999&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* App A: announcement bar */&lt;/span&gt;
&lt;span class="nc"&gt;.announcement-bar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* beats header */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* App B: chat widget */&lt;/span&gt;
&lt;span class="nc"&gt;.chat-bubble&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9999&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* beats announcement */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* App C: email signup popup */&lt;/span&gt;
&lt;span class="nc"&gt;.signup-overlay&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;90000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* beats everything */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Works: Scoped Selectors
&lt;/h2&gt;

&lt;p&gt;The core principle that separates high-scoring stores from low-scoring ones: app CSS should not compete in the theme's specificity range.&lt;/p&gt;

&lt;p&gt;The cleanest pattern is a data-attribute scoped container. Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* WRONG: competes with theme class selectors */&lt;/span&gt;
&lt;span class="nc"&gt;.review-widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* RIGHT: container adds specificity, content is predictable */&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-app-reviews&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nc"&gt;.review-widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;You scope yourself, so you don't need !important.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-app-reviews&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nc"&gt;.rw-header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-app-reviews&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nc"&gt;.rw-body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-app-reviews&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nc"&gt;.rw-footer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every selector in your stylesheet stays at 0-2-0 or below. No conflict, no escalation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Works: CSS Layers
&lt;/h2&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@layer&lt;/span&gt; &lt;span class="n"&gt;app-reviews&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.review-widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What Works: App Blocks (Best Option)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most conflict-proof approach is Shopify's app blocks extension — not injecting CSS at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="cp"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;comment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="c"&gt; sections/app-reviews.liquid &lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endcomment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;schema&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;%}&lt;/span&gt;
{
  "name": "Product Reviews",
  "target": "section"
}
&lt;span class="cp"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endschema&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;%}&lt;/span&gt;

&amp;lt;div class="review-widget" data-product-id="&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;"&amp;gt;
  &amp;lt;!-- Widget content --&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;App blocks render inside the theme's section rendering pipeline. The theme controls CSS scoping. You don't inject styles — you rely on the theme's existing component system.&lt;/p&gt;

&lt;p&gt;The trade-off: app blocks require Shopify's extension architecture and not all app categories support them yet. But for reviews, product options, size charts, and other embedded widget types, they're the most durable solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Doesn't Work: Bare Class Selectors
&lt;/h2&gt;

&lt;p&gt;Stop shipping bare class selectors in your app's injected stylesheet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Don't do this */&lt;/span&gt;
&lt;span class="nc"&gt;.review-widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.product-form&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.add-to-cart&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are too generic. They're in the theme's specificity range. They'll either lose to the theme or win and create regressions. Both outcomes generate support tickets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Specificity Anti-Patterns by App Category
&lt;/h2&gt;

&lt;p&gt;From our 53-store scan, CSS specificity conflicts are most prevalent in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reviews widgets&lt;/strong&gt; — most common target, highest conflict rate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Announcement bars and promo popups&lt;/strong&gt; — z-index escalation problem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size/fit recommendation widgets&lt;/strong&gt; — inject near the add-to-cart form, cause layout shifts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loyalty/reward point widgets&lt;/strong&gt; — often inject as overlays, highest !important rate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live chat/chatbot widgets&lt;/strong&gt; — lowest specificity conflict but create z-index problems&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're building an app in one of these categories, your CSS specificity strategy is load-bearing. The data from 53 stores is unambiguous: app CSS that doesn't account for theme specificity is the single most common source of regressions we found.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scope your CSS to your container element.&lt;/strong&gt; Add a data attribute to your injected element and scope every rule to it. Your specificity becomes predictable and self-contained.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use @layer to negotiate cascade priority without !important.&lt;/strong&gt; Layers give you explicit control over precedence without forcing specificity conflicts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stop using bare class selectors for injected widgets.&lt;/strong&gt; Generic selectors compete with the theme and either lose or create regressions. Always scope.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;For popup/overlay widgets, coordinate z-index with CSS custom properties.&lt;/strong&gt; Don't pick a large arbitrary number — read the theme's --header-z-index or equivalent and set yours relative to it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test with 3+ apps installed, not alone.&lt;/strong&gt; Your CSS behaves differently when other apps are injecting conflicting rules. Your test store should reflect that.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We've published the full conflict dataset — including per-store specificity breakdowns, z-index distribution, and app category conflict rates — at &lt;a href="https://preflight.technology/insights?utm_source=reddit&amp;amp;utm_medium=organic&amp;amp;utm_campaign=css_specificity_wars" rel="noopener noreferrer"&gt;preflight.technology/insights&lt;/a&gt;. No signup required to browse the aggregate patterns. If you're debugging a specific app's compatibility or want to see how your store scores against the 87-point average from our dataset, the per-app conflict lookup is there too.&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>css</category>
      <category>javascript</category>
    </item>
    <item>
      <title>What We Found Scanning 53 Shopify Stores for App-Theme Conflicts</title>
      <dc:creator>Jeff Thomas</dc:creator>
      <pubDate>Mon, 18 May 2026 14:00:50 +0000</pubDate>
      <link>https://dev.to/jthomas2/what-we-found-scanning-53-shopify-stores-for-app-theme-conflicts-2h</link>
      <guid>https://dev.to/jthomas2/what-we-found-scanning-53-shopify-stores-for-app-theme-conflicts-2h</guid>
      <description>&lt;p&gt;We built a tool that scans Shopify storefronts for compatibility issues between third-party apps and themes. Over the past two months, we pointed it at 53 live stores — all running at least three third-party apps — and logged every conflict we could detect.&lt;/p&gt;

&lt;p&gt;Here's the raw data:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;53 stores&lt;/strong&gt; scanned&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;87/100&lt;/strong&gt; average compatibility score&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;147+ critical issues&lt;/strong&gt; flagged&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.5x more rendering problems&lt;/strong&gt; in stores running 3+ apps vs. 1–2&lt;/p&gt;

&lt;p&gt;An 87 average sounds fine. It's not. The distribution is bimodal: most stores either score 92+ (minimal issues) or drop below 75 (multiple broken interactions). The middle ground is thin. And the stores below 75? They almost always have the same three problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern #1: CSS Specificity Wars (70% of Issues)
&lt;/h2&gt;

&lt;p&gt;This was the single biggest source of conflicts. Roughly 70% of every critical issue we flagged traced back to CSS specificity.&lt;/p&gt;

&lt;p&gt;The pattern is predictable. A reviews app injects styles like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.review-widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;24px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean, reasonable CSS. The problem is the theme already has this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nf"&gt;#shopify-section-template--product&lt;/span&gt; &lt;span class="nc"&gt;.product__block&lt;/span&gt; &lt;span class="nc"&gt;.review-widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inherit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The theme selector has a specificity of 1-2-1. The app selector has 0-1-0. The theme wins every time, and the review widget renders as a compressed, unreadable block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What app developers do next makes it worse.&lt;/strong&gt; They escalate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.review-widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;24px&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This "fixes" the reviews — and breaks the store's carefully tuned spacing on mobile. We found !important declarations in 31 of the 53 stores we scanned. In 19 of those, the !important rules created new layout issues that weren't present before the app was installed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The z-index arms race
&lt;/h2&gt;

&lt;p&gt;The second CSS pattern we saw constantly was z-index stacking conflicts. Here's a real example from our dataset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Theme: sticky header */&lt;/span&gt;
&lt;span class="nc"&gt;.header__wrapper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sticky&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;999&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* App A: notification bar */&lt;/span&gt;
&lt;span class="nc"&gt;.announcement-bar-widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* App B: chat widget */&lt;/span&gt;
&lt;span class="nc"&gt;.chat-bubble-container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9999&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* App C: email signup popup */&lt;/span&gt;
&lt;span class="nc"&gt;.signup-modal-overlay&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;90000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four apps. Four different z-index strategies. Zero coordination. The result: the signup popup renders above everything (including the close button of the chat widget), the notification bar covers the header on scroll, and the merchant has no idea why their mobile UX feels broken.&lt;/p&gt;

&lt;p&gt;We found z-index values exceeding 10,000 in 23 of 53 stores. Five stores had z-index values above 90,000.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually works
&lt;/h2&gt;

&lt;p&gt;For app developers building Shopify theme extensions, scope your CSS aggressively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Instead of this: */&lt;/span&gt;
&lt;span class="nc"&gt;.review-widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Do this: */&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-app-reviews&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nc"&gt;.review-widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* Scoped to your app's container */&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;24px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're targeting modern browsers (and you should be — Shopify's audience skews current), CSS @layer is a cleaner solution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@layer&lt;/span&gt; &lt;span class="n"&gt;app-reviews&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.review-widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;24px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Layers let your styles participate in the cascade without brute-forcing specificity. The theme's unlayered styles will still win by default, but you can negotiate precedence without !important.&lt;/p&gt;

&lt;p&gt;Pattern #2: JavaScript Event Handler Collisions&lt;/p&gt;

&lt;p&gt;This accounted for about 20% of the critical issues we flagged. The pattern: multiple apps binding event listeners to the same DOM elements, usually around cart interactions.&lt;/p&gt;

&lt;p&gt;Here's what we saw in one store running a wishlist app, a reviews app, and an upsell widget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// App A: Wishlist — intercepts add-to-cart&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;form[action="/cart/add"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// Track wishlist state, then submit&lt;/span&gt;
    &lt;span class="nf"&gt;trackWishlistConversion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// App B: Upsell — also intercepts add-to-cart&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;form[action="/cart/add"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// Show upsell modal, then submit&lt;/span&gt;
    &lt;span class="nf"&gt;showUpsellModal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both apps call preventDefault(). Both expect to be the only one controlling the submit flow. The result: the add-to-cart button either fires twice, shows the upsell modal but never adds the product, or silently fails with no feedback.&lt;/p&gt;

&lt;p&gt;We found this exact collision pattern — multiple preventDefault() calls on the same form — in 11 of 53 stores.&lt;/p&gt;

&lt;h2&gt;
  
  
  Script loading order is the other half
&lt;/h2&gt;

&lt;p&gt;Apps that depend on jQuery (yes, still) face a loading order problem. The theme loads jQuery deferred. The app's inline script executes immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Theme: deferred jQuery --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"jquery-3.6.0.js"&lt;/span&gt; &lt;span class="na"&gt;defer&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- App: inline script that assumes jQuery exists --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;jQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#product-reviews&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.review-star&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// This throws: jQuery is not defined&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fails silently in production. The review stars don't work. The merchant doesn't know why. The app developer's test store (with jQuery loaded synchronously) works fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually works
&lt;/h2&gt;

&lt;p&gt;Use custom events instead of hijacking native form submissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Emit a custom event instead of intercepting submit&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart:before-add&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// Do your thing without blocking other listeners&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Coordinate with other apps&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cartEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CustomEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart:before-add&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;cancelable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cartEvent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And drop jQuery. Vanilla JS event listeners with defer solve the loading order problem entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="nx"&gt;defer&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-app.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
&lt;span class="c1"&gt;// your-app.js — runs after DOM is ready because of defer&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#product-reviews&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleReviewClick&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pattern #3: DOM Mutation Conflicts
&lt;/h2&gt;

&lt;p&gt;The remaining ~10% of critical issues came from apps that make assumptions about DOM structure. When the theme updates, those assumptions break.&lt;/p&gt;

&lt;p&gt;A typical example: an app targets a specific class to inject its widget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// App expects this structure&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.product__info-container&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertAdjacentHTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;afterend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;widgetHTML&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Theme updates from Dawn 12.0 to 13.0. The class changes to .product__info-wrapper. The app injects nothing. No error, no fallback — just a missing widget.&lt;/p&gt;

&lt;p&gt;We tracked stores across multiple scans and found that theme updates caused at least one app widget to disappear in 8 of the 53 stores during our observation window.&lt;/p&gt;

&lt;h2&gt;
  
  
  The MutationObserver pileup
&lt;/h2&gt;

&lt;p&gt;Worse, some apps use MutationObserver to watch for DOM changes and re-inject their widgets. When three apps all observe document.body for childList changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Three separate MutationObservers, all watching body&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MutationObserver&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;mutations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;mutations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// App re-injects widget on every DOM change&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.my-widget&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;injectWidget&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;childList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;subtree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each injection triggers the other observers. In the worst case we found, this created a tight re-render loop that pushed page load time from 2.1s to 6.8s.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually works
&lt;/h2&gt;

&lt;p&gt;Use Shopify's app blocks and theme app extensions. They're designed for exactly this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="cp"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;comment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="c"&gt; blocks/review-widget.liquid &lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endcomment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;schema&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;%}&lt;/span&gt;
{
  "name": "Product Reviews",
  "target": "section"
}
&lt;span class="cp"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endschema&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;%}&lt;/span&gt;

&amp;lt;div class="review-widget" data-product-id="&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;"&amp;gt;
  &amp;lt;!-- Widget content --&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;App blocks let merchants place your widget in the theme editor. No DOM queries, no fragile class selectors, no MutationObserver hacks. When the theme updates, the block stays where the merchant put it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;The stores that scored 92+ in our scans had a common thread: they used fewer apps (3–4 vs. 6+), and the apps they did use were built with Shopify's modern extension architecture.&lt;/p&gt;

&lt;p&gt;The stores below 75 were running 5+ apps, most injecting unscoped CSS and attaching event listeners directly to DOM elements. The conflicts weren't bugs in any single app — they were emergent behavior from apps that were never tested together.&lt;/p&gt;

&lt;p&gt;If you're building Shopify apps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scope your CSS — use data attributes or CSS layers, not bare class selectors&lt;/li&gt;
&lt;li&gt;Don't hijack native events — emit custom events and let other apps coordinate&lt;/li&gt;
&lt;li&gt;Use app blocks — stop querying the DOM for injection points&lt;/li&gt;
&lt;li&gt;Test with other apps installed — your app will never run alone in production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We've published the aggregate data from our scans — broken down by conflict type, app category, and theme — at &lt;a href="//preflight.technology/insights?utm_source=devto&amp;amp;utm_medium=article"&gt;preflight.technology/insights&lt;/a&gt;. No signup required to browse the patterns. If you're debugging a specific app's compatibility, the per-app conflict lookup is there too.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>css</category>
      <category>webdev</category>
      <category>shopify</category>
    </item>
  </channel>
</rss>
