Files
hermes-config/skills/software-development/web-ui-repair/references/dropdown-teleport-pattern.md
T
2026-07-12 10:17:17 -04:00

2.3 KiB

Dropdown Portal / Teleport Pattern

When a dropdown is clipped by a parent with overflow: hidden or trapped inside a stacking context, CSS z-index alone cannot fix it. The solution is to move the dropdown to a portal root at the end of <body> and position it with position: fixed + getBoundingClientRect().

Setup

Every page needs <div id="dropdown-root"></div> right before </body>. This is a portal target — a direct child of <body> with no clipping ancestors.

Vanilla JS Implementation

function showDropdown(dropdown, anchorInput) {
  // 1. Move to portal
  const root = document.getElementById('dropdown-root');
  if (root && dropdown.parentElement !== root) {
    root.appendChild(dropdown);
  }

  // 2. Position relative to anchor
  const rect = anchorInput.getBoundingClientRect();
  dropdown.style.position = 'fixed';
  dropdown.style.top = (rect.bottom + 4) + 'px';
  dropdown.style.left = rect.left + 'px';
  dropdown.style.width = rect.width + 'px';
  dropdown.style.zIndex = '99999';
  dropdown.classList.remove('hidden');
}

// 3. Reposition on scroll/resize
window.addEventListener('scroll', () => {
  if (!dropdown.classList.contains('hidden')) {
    const rect = anchorInput.getBoundingClientRect();
    dropdown.style.top = (rect.bottom + 4) + 'px';
    dropdown.style.left = rect.left + 'px';
    dropdown.style.width = rect.width + 'px';
  }
}, { passive: true });

React Implementation

import { createPortal } from 'react-dom';

function Dropdown({ open, anchorRect, children }) {
  if (!open) return null;
  return createPortal(
    <div style={{
      position: 'fixed',
      top: anchorRect.bottom + 4,
      left: anchorRect.left,
      width: anchorRect.width,
      zIndex: 99999,
    }}>
      {children}
    </div>,
    document.getElementById('dropdown-root')!
  );
}

Why z-index alone fails

A parent with overflow: hidden, transform, will-change, or position: relative creates a new stacking context. The dropdown's z-index is scoped within that parent — it can never escape to be above a sibling element that comes later in the DOM.

Diagnosis

If z-index: 99999 !important on the dropdown doesn't fix clipping, and the dropdown is inside any ancestor with overflow: hidden or position: relative, this is the fix.