The CMS bottom bar can't just sit at bottom: 0 โ it has to coexist with the app's own bottom bar, GDPR/cookie banners, sticky chat bubbles, and other floating UI. Detection is positional + semantic. The bar moves to the safest empty slot.
S-1 ยท Hero
S-3 ยท Services
S-5 ยท About
Message list...
More messages...
S-1 ยท Hero
S-3 ยท Services
S-1 ยท Hero
S-3 ยท Services
| Conflict | Detected by | CMS bar action | Visual signal |
|---|---|---|---|
| App bottom bar (nav, tab bar) | position: fixed/sticky; bottom: 0 + role=navigation or height > 40px |
Stack above the app bar with 4-8px gap | Normal opacity, sits at e.g. bottom: 56px |
| GDPR / cookie banner | Common class names: .gdpr-banner, .cookie-banner, .consent, [data-gdpr] or text match ("cookies", "consent", "privacy") |
Hide until user dismisses GDPR. Re-evaluate on dismiss event | CMS bar slides in from bottom, fades in |
| Sticky chat bubble | position: fixed + right < 30px + bottom < 30px + circular/round element |
Move CMS bar to top or left side as a pill | Compact pill, top-right or top-center |
| Sticky CTA / promo bar | position: fixed/sticky; bottom: 0 + button or anchor inside |
Stack above it OR move to top | Same as app bar handling |
| Back-to-top button | Round button, right + bottom < 50px |
No conflict โ sits in different slot | CMS bar unchanged |
| Toast notifications | role=alert or .toast, animated in |
CMS bar yields for 4s while toast is visible, then reclaims | CMS bar slides down temporarily |
| Modal / dialog | role=dialog or .modal with backdrop |
CMS bar hides while modal is open (z-index < modal's) | Reappears on modal close |
// freshvibe-cms/cms/placement.js function detectConflicts() { const conflicts = { appBar: null, // app's own bottom nav gdpr: null, // GDPR / cookie banner chatBubble: null, // sticky chat bubble cta: null, // sticky CTA / promo toast: null, // transient toast modal: null, // modal dialog }; // Walk all elements with position: fixed or sticky const stickies = querySelectorAll('*').filter(el => { const s = getComputedStyle(el); return s.position === 'fixed' || s.position === 'sticky'; }); for (const el of stickies) { const r = el.getBoundingClientRect(); const s = getComputedStyle(el); const atBottom = r.bottom > window.innerHeight - 100; const touchesRight = r.right > window.innerWidth - 80; // GDPR / cookie: text match + position if (atBottom && (matchesGdpr(el) || textMatches(el, ['cookie','consent','gdpr','privacy']))) { conflicts.gdpr = el; } // App bar: nav, tab bar, height > 40 else if (atBottom && (el.getAttribute('role') === 'navigation' || el.matches('.tab-bar, .bottom-nav, nav.app-nav') || (r.height >= 40 && r.width > 200))) { conflicts.appBar = el; } // Chat bubble: round, small, bottom-right else if (r.width < 80 && r.height < 80 && touchesRight && r.bottom > window.innerHeight - 100) { conflicts.chatBubble = el; } // CTA / promo: bottom-anchored with button else if (atBottom && el.querySelector('a, button')) { conflicts.cta = el; } } return conflicts; } function resolvePlacement(conflicts) { // GDPR wins โ CMS bar defers until dismissed if (conflicts.gdpr && conflicts.gdpr.offsetParent !== null) { return { where: 'hidden', reason: 'gdpr-active' }; } // Modal open โ yield if (conflicts.modal) return { where: 'hidden' }; // Toast visible โ yield temporarily if (conflicts.toast) return { where: 'hidden-until-toast-clears' }; // App bar present โ stack above if (conflicts.appBar) { const h = conflicts.appBar.getBoundingClientRect().height; return { where: 'bottom-stacked', offsetY: h + 4 }; } // Chat bubble or CTA near bottom-right โ move to top if (conflicts.chatBubble || conflicts.cta) { return { where: 'top-floating' }; } // Default: bottom return { where: 'bottom' }; }
0โ1000 โ page content1000โ5000 โ app's own UI (nav, dropdowns)5000โ9000 โ chat bubbles, sticky CTAs9000โ10000 โ toasts, transient10000+ โ modals, GDPR (always on top until dismissed)9999 โ CMS bar (above everything except GDPR/modals)