PF
PreFlight
Storefront Diagnostic Engine
PreFlight Guides

How to Debug Shopify Theme Conflicts
A Guide for App Developers

Your app works perfectly in isolation. Install it on a merchant's store and suddenly the widget is invisible, the button is unclickable, or the popup is cut off. This guide explains exactly why that happens — and how to fix it fast.

📖 12 min read · Updated March 2026 · PreFlight Team

In This Guide

  1. 1.What Causes Theme Conflicts
  2. 2.Manual Diagnosis with Browser DevTools
  3. 3.Automated Diagnostics with PreFlight
  4. 4.Common Fixes with Code Snippets
  5. 5.Prevention Best Practices

01 What Causes Theme Conflicts

Theme conflicts aren't random. They fall into four predictable categories. Once you know which bucket your bug is in, diagnosis goes from hours to minutes.

Z-Index Stacking Wars

Shopify themes — especially popular ones like Dawn, Debut, and Debutify — define their own stacking contexts. A theme might set z-index: 99999 on its sticky header or cart drawer. If your app's widget sits in the same stacking context with a lower z-index, it gets buried.

The subtler problem: stacking contexts reset the z-index scale. An element with position: relative; z-index: 1 on a parent traps all children in that context — no matter how high you set their z-index, they can never escape above a sibling context.

/* The theme's sticky header traps your widget */ .theme-sticky-header { position: fixed; z-index: 100; /* Creates a stacking context */ } /* Your app widget — trapped inside this context */ .your-app-widget { z-index: 99999; /* Doesn't matter — it's inside a z:100 context */ }
Common symptom: Your widget appears in the DOM and has correct CSS, but it's visually hidden behind the theme's header, cart drawer, or cookie banner.

overflow:hidden Clipping

This is the most frustrating one because it's invisible in DevTools at first glance. A theme wraps its product grid, hero section, or page container with overflow: hidden to prevent horizontal scroll or contain floated elements.

Any element positioned outside that container's bounds — like a tooltip, dropdown, or badge that hangs off the edge — gets clipped. It doesn't scroll. It doesn't overflow. It just disappears.

/* Theme section — clips everything outside bounds */ .shopify-section { overflow: hidden; position: relative; } /* Your badge renders at position: absolute; bottom: -20px */ /* It's clipped — user sees nothing, no error in console */
Common symptom: Element appears in DOM inspection, computed styles look correct, but it's not visible on the page. Toggling overflow: hidden on the parent in DevTools makes it reappear.

display:none and visibility:hidden

Themes hide elements conditionally — for mobile breakpoints, loading states, or A/B tests. If your app injects into a container that a theme stylesheet hides with a broad selector, your element inherits that hidden state.

The tricky case: the theme uses visibility: hidden on a parent (for animation purposes), which propagates to all children — even ones with explicit visibility: visible. Unlike opacity, you can override this by setting visibility explicitly on the child.

/* Theme: broad selector hides elements during page load */ .page-container > * { visibility: hidden; } /* Theme JS reveals them when "loaded" class fires */ .page-container.loaded > * { visibility: visible; } /* Your widget injected AFTER load fires — misses the trigger */ /* Fix: scope your injection or set visibility explicitly */

CSS Specificity Conflicts

Theme stylesheets are loaded globally and often use high-specificity selectors to style product pages, forms, and buttons. If your app's styles use similar or lower-specificity selectors, the theme wins — even if your CSS loads later in the page.

Specificity is calculated as IDs (100) → Classes/Attributes (10) → Elements (1). A theme rule like .product-form button.btn-primary (specificity: 21) beats your app's .my-app-btn (specificity: 10) every time.

/* Theme: specific selector (specificity = 0-2-1 = 21) */ .product-form button.btn-primary { background: #000; color: #fff; } /* Your app: lower specificity (0-1-0 = 10) — loses the fight */ .my-app-cta { background: #22c55e; /* Never applied */ }

02 Manual Diagnosis with Browser DevTools

Chrome DevTools is your primary weapon. Here's a systematic walkthrough — takes about 15 minutes on an unfamiliar storefront.

1

Open DevTools on the merchant's live storefront

Navigate to the merchant's store with your app installed. Open DevTools with F12 or Cmd+Option+I. Always test on the page type where your app appears — product pages, cart, homepage — not just any page.

2

Confirm the element exists in the DOM

Use the Elements panel (or Ctrl+F in Elements) to search for your app's unique class name or ID. If it's not there at all, the issue is JavaScript execution — not CSS.

/* In the DevTools Console, confirm element exists: */ document.querySelector('.your-app-widget') // → null means JS injection failed // → HTMLElement means the element is there, problem is CSS
3

Check Computed Styles for display and visibility

In the Elements panel, select your element. Click the Computed tab (not Styles). Filter for display, visibility, and opacity. If any of these are hiding the element, it'll show here — even if the inherited value came from a parent 5 levels up.

Pro tip: In the Computed tab, click the arrow next to a property value to see which CSS rule is responsible and jump directly to it in the Styles panel.
4

Trace overflow:hidden up the DOM tree

If computed styles look fine but the element is still invisible or cut off, the culprit is probably a parent with overflow: hidden. Run this in the console to find it:

// Find which ancestor has overflow:hidden function findOverflowHidden(el) { let node = el.parentElement; while (node) { const style = window.getComputedStyle(node); if (style.overflow === 'hidden' || style.overflowX === 'hidden' || style.overflowY === 'hidden') { console.log('overflow:hidden found on:', node); return node; } node = node.parentElement; } console.log('No overflow:hidden found in ancestors'); } findOverflowHidden(document.querySelector('.your-app-widget'));
5

Inspect z-index stacking context

Select your element in the Elements panel. In the Styles tab, look at what z-index is applied and whether it's being overridden. Then walk up the DOM tree looking for a parent that creates a new stacking context (any element with position + z-index, or transform, opacity < 1, filter, or will-change).

// Check if element's stacking context is trapped function findStackingContext(el) { let node = el.parentElement; while (node && node !== document.body) { const s = window.getComputedStyle(node); const isContext = (s.position !== 'static' && s.zIndex !== 'auto') || s.transform !== 'none' || parseFloat(s.opacity) < 1 || s.filter !== 'none' || s.willChange !== 'auto'; if (isContext) console.log('Stacking context:', node, s.zIndex); node = node.parentElement; } } findStackingContext(document.querySelector('.your-app-widget'));
6

Find competing CSS rules with Styles panel

In the Styles panel, any rule that's been overridden shows with a strikethrough. Rules are listed from highest to lowest specificity. If a theme rule is winning, you'll see your app's rule struck through with the theme's rule above it.

Click the file:line reference next to any rule to jump to the exact stylesheet and line — this tells you whether it's coming from the theme, a Shopify section, or another app's injected CSS.

03 Automated Diagnostics with PreFlight

The manual process above works — but it requires access to DevTools on a live merchant's store, which usually means a screen share call. When a merchant reports "your app broke my site," you want answers before that call, not during it.

PreFlight runs all six diagnostic checks automatically against any Shopify storefront URL: Flair badge/banner rendering, display/visibility conflicts, z-index stacking issues, overflow clipping, CSS rule conflicts, and third-party app conflicts (Loox, Yotpo, Judge.me, and others).

Paste URL → get a health score in under 3 seconds

PreFlight fetches the storefront, analyzes the computed styles and DOM structure, and returns a 0–100 health score with specific issues flagged — including which CSS rule is responsible and a suggested fix. No DevTools access required, no screen share needed.

When a support ticket comes in, paste the store URL, scan it before the merchant call, and walk into the conversation already knowing where the conflict is. The diagnostic output includes copy-paste CSS fixes for every issue found.

04 Common Fixes with Code Snippets

Once you've identified the conflict type, here are the standard fixes. These are battle-tested patterns that work across Dawn, Debut, Impulse, and most popular Shopify themes.

Fix 1: Z-Index Conflicts

If your element is buried under a theme header or cart drawer

The safest fix is to move your widget to a new stacking context at the document.body level — not nested inside any theme containers. Then set a sufficiently high z-index.

/* In your app's injected stylesheet: */ /* Step 1: Move widget to body level (do this in JS) */ // document.body.appendChild(yourWidgetElement) /* Step 2: Position it absolutely from the viewport */ .your-app-widget { position: fixed; /* or absolute if not a floating element */ z-index: 2147483647; /* INT_MAX — safe for fixed/absolute at body level */ isolation: isolate; /* creates own stacking context */ }
Important: position: fixed already creates a stacking context relative to the viewport — you don't need to fight theme z-index values if your element is positioned fixed at body level.

Fix 2: overflow:hidden Clipping

If your element is getting clipped by a parent container

Option A: Move the element out of the clipping container (cleanest fix).

// Move element to body or a non-clipping ancestor const widget = document.querySelector('.your-app-widget'); const parent = widget.parentElement; // Store original position for absolute placement const rect = parent.getBoundingClientRect(); document.body.appendChild(widget); widget.style.position = 'fixed'; widget.style.top = rect.bottom + 'px'; widget.style.left = rect.left + 'px';

Option B: Override the parent's overflow (riskier — can break theme layout).

/* Override theme overflow — use with caution */ /* Scope tightly to avoid breaking the theme's scroll behavior */ .shopify-section:has(.your-app-widget) { overflow: visible !important; }
Warning: Overriding overflow: hidden on theme sections can expose horizontal scroll issues the theme was hiding. Always test on mobile after this change.

Fix 3: display:none and visibility Inheritance

If your element is hidden due to theme loading states or conditional CSS

/* Override hidden state with higher specificity */ /* Using :is() allows multiple selectors at once */ :is(.page-container, .product-section, body) .your-app-widget { display: block !important; visibility: visible !important; opacity: 1 !important; } /* For animations using visibility:hidden on parent: */ .your-app-widget { visibility: visible; /* children CAN override inherited visibility */ }

For loading-state conflicts (where the theme hides elements until JS runs), ensure your app's injection happens after the theme's initialization event — not just after DOMContentLoaded:

// Wait for theme to finish its setup before injecting function injectAfterTheme(callback) { // Shopify themes often dispatch a custom event when ready if (document.documentElement.classList.contains('js')) { callback(); // Theme already done } else { document.addEventListener('page:load', callback); // Theme Kit document.addEventListener('shopify:section:load', callback); // Section events window.addEventListener('load', callback); // Fallback } }

Fix 4: CSS Specificity Overrides

When theme rules are winning the specificity war

The cleanest approach is to scope your styles with a high-specificity wrapper that doesn't depend on theme markup:

/* Approach 1: ID-level scoping (specificity boost without !important) */ #your-app-root .your-app-btn { /* specificity: 1-1-0 = 110 — beats most theme selectors */ background: #22c55e; color: #000; } /* Approach 2: CSS Layers (modern — lets you control cascade order) */ @layer theme, app; @layer app { .your-app-btn { background: #22c55e; /* app layer always wins over theme layer */ } } /* Approach 3: Shadow DOM (nuclear option — full isolation) */ const shadow = container.attachShadow({ mode: 'open' }); shadow.innerHTML = ` <style>/* your styles here — completely isolated */</style> <div class="your-app-btn">...</div> `;
Recommendation: Use Shadow DOM for UI-heavy apps (badges, widgets, review forms). It's the only true isolation. Use CSS layers for lightweight injections where Shadow DOM is overkill.

Fix 5: Third-Party App Conflicts

When another app's CSS or JS is stomping on yours

Loox, Yotpo, Judge.me, and other review/upsell apps inject global CSS with low-specificity selectors that often collide with similar apps. Check for these specific patterns:

// Find all stylesheets injected by other apps Array.from(document.styleSheets) .filter(ss => ss.href && !ss.href.includes('cdn.shopify.com')) .forEach(ss => console.log('Third-party stylesheet:', ss.href)); // Find conflicting rules for a specific property Array.from(document.styleSheets).forEach(ss => { try { Array.from(ss.cssRules || []).forEach(rule => { if (rule.selectorText?.includes('your-class')) { console.log(rule.cssText, 'from', ss.href); } }); } catch (e) {} // Cross-origin sheets throw SecurityError });

05 Prevention Best Practices

The best theme conflict is one that never happens. Build defensively from the start.

🛡️

Use Shadow DOM for UI

Any user-facing UI element — badges, popups, widgets, review forms — should render inside a Shadow DOM. Zero CSS leakage in or out.

🎯

Append to body, not theme sections

Floating elements (modals, toasts, overlays) should be direct children of document.body. Never inject into .shopify-section containers.

📦

Namespace everything

Prefix all class names, IDs, and CSS custom properties with your app identifier. .yapp-btn instead of .btn.

🔬

Test on 3 themes before launch

Dawn (default), Debut (legacy), and one third-party theme (Impulse or Prestige). Covers 80% of storefronts you'll encounter.

Scan before merchant calls

When a support ticket comes in, run a diagnostic scan first. Walk into the call with the issue and fix already identified.

📋

Avoid global resets

Never ship a CSS reset that targets bare HTML elements (*, body, h1). It will break theme typography on every install.

Quick Reference: Symptom → Root Cause → Fix

Symptom Root Cause Quick Fix
Element in DOM, invisible, no console errors overflow:hidden on ancestor Move to body level
Widget buried under sticky header Z-index stacking context position:fixed at body level
Styles not applying, theme wins CSS specificity loss ID scoping or Shadow DOM
Element hidden on page load, visible later Theme loading state Inject after window:load
Conflict only with certain other apps Third-party CSS collision Namespace + Shadow DOM
Works in Chrome, broken in Safari CSS Grid/flexbox rendering diff Add -webkit- prefixes

PreFlight — Storefront Diagnostic Engine

Stop guessing. Scan the storefront.

Paste any Shopify store URL and get a health score + specific conflict analysis in under 3 seconds. Free, no account required.

Try PreFlight Free

No account needed — just paste a URL