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.
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.
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 */
}
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 */
overflow: hidden on the parent in DevTools makes it reappear.
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 */
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 */
}
Chrome DevTools is your primary weapon. Here's a systematic walkthrough — takes about 15 minutes on an unfamiliar 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.
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
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.
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'));
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'));
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.
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.
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.
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 */
}
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.
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;
}
overflow: hidden on theme sections can expose horizontal scroll issues the theme was hiding. Always test on mobile after this change.
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
}
}
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>
`;
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
});
The best theme conflict is one that never happens. Build defensively from the start.
Any user-facing UI element — badges, popups, widgets, review forms — should render inside a Shadow DOM. Zero CSS leakage in or out.
Floating elements (modals, toasts, overlays) should be direct children of document.body. Never inject into .shopify-section containers.
Prefix all class names, IDs, and CSS custom properties with your app identifier. .yapp-btn instead of .btn.
Dawn (default), Debut (legacy), and one third-party theme (Impulse or Prestige). Covers 80% of storefronts you'll encounter.
When a support ticket comes in, run a diagnostic scan first. Walk into the call with the issue and fix already identified.
Never ship a CSS reset that targets bare HTML elements (*, body, h1). It will break theme typography on every install.
| 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
Paste any Shopify store URL and get a health score + specific conflict analysis in under 3 seconds. Free, no account required.
Try PreFlight FreeNo account needed — just paste a URL