Bottom bar placement + GDPR coexistence

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.

๐Ÿ“ฑ Four scenarios โ€” where the CMS bar lands
PLACEMENT
1. Clean page โ€” no other bars CMS bar: bottom: 0

Tree Academy

S-1 ยท Hero

S-3 ยท Services

S-5 ยท About

CMS: 0
2. App has its own bottom bar (chat/music) CMS bar: stacked ABOVE app bar

My App

Message list...

More messages...

๐Ÿ“ฅInbox
๐Ÿ’ฌChats
๐Ÿ“žCalls
โš™๏ธSettings
CMS: 50px
3. GDPR cookie banner โ€” visible CMS bar: hidden (banner is sacred)

Tree Academy

S-1 ยท Hero

S-3 ยท Services

๐Ÿช We use cookies to make this site work. Learn more
CMS: dimmed + above GDPR
4. Multiple overlays (chat + cookie + CTA) CMS bar: floats top-right as mini-pill

Tree Academy

S-1 ยท Hero

S-3 ยท Services

๐Ÿ’ฌ
CMS: top pill
๐Ÿง  Detection โ€” what the bar scans for
ALGORITHM
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
โš™๏ธ The detection + placement function
CODE
// 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' };
}

Why the bar has to be smart, not greedy

Re-evaluation triggers

Z-index ladder

What the operator sees