Das Dropdown, das sich nicht anklicken ließ
Ein Menü wurde sauber gerendert, saß genau da, wo es hingehörte, und schluckte jeden Klick. Schuld war eine Einblendanimation, die nie richtig endete. Eine kurze Führung durch CSS-Stapelkontexte, Fill-Modes und die Jagd auf Fehler, die man nicht sehen kann.
Wir haben ein Dropdown-Menü ausgeliefert, das sich nicht anklicken ließ. Es öffnete sauber. Es wurde an der richtigen Stelle gerendert, voll sichtbar, mit korrektem z-index, ohne Fehler in der Konsole. Klicks gingen mitten hindurch in das Panel darunter, als wäre das Menü ein Geist. Wer je gegen so einen Fehler gekämpft hat, kennt den verstörenden Teil: Alles sieht richtig aus, und nichts funktioniert.
Herausfinden, wer wirklich obenauf liegt
Die erste ehrliche Antwort kam vom Browser, nicht vom Lesen des Codes. document.elementFromPoint sagt dir, welches Element an einer Koordinate den Klick tatsächlich bekommt. Wir haben es auf die Mitte eines Menüeintrags gerichtet, und zurück kam eine Karte, die weiter hinten auf der Seite saß. Das Menü lag optisch oben und körperlich darunter.
// Who really gets the click at this point?const item = document.querySelector('[role="menuitem"]');const r = item.getBoundingClientRect();const top = document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2);console.log(top === item || item.contains(top)); // false -> something covers it // Walk the ancestors and list everything that creates a stacking contextfor (let el = item; el; el = el.parentElement) { const cs = getComputedStyle(el); const reasons = []; if (cs.transform !== 'none') reasons.push('transform'); if (cs.filter !== 'none') reasons.push('filter'); if (cs.backdropFilter !== 'none') reasons.push('backdrop-filter'); if (cs.opacity !== '1') reasons.push('opacity'); if (cs.position !== 'static' && cs.zIndex !== 'auto') reasons.push('z-index'); if (reasons.length) console.log(el.className, reasons);}Der Durchlauf hat es gefunden. Der Header, in dem das Menü lag, meldete ein transform, obwohl kein Stylesheet ihm eines gab. Das einzige transform in seiner Nähe war eine Einblendanimation, ein kleines Auf-und-Ein, das eine halbe Sekunde nach dem Laden fertig war.
Fill-Modes überleben ihre Animation
animation-fill-mode: both heißt, dass das Element vor dem Start den ersten Keyframe hält und nach dem Ende den letzten Keyframe behält. Behält, für immer. Animiert irgendein Keyframe transform, wendet die fertige Animation weiterhin ein transform an, und ein transformiertes Element wird zum Stapelkontext. Der z-index jedes Nachfahren sitzt jetzt darin fest und tritt nur noch gegen die lokalen Geschwister an, nie gegen den Rest der Seite.
Auf unserer Seite kamen nach dem Header Panels mit backdrop-filter, und das erzeugt einen eigenen Stapelkontext. Zwei versiegelte Kisten, und über die Reihenfolge beim Zeichnen entschied die Baumstruktur, nicht der z-index, den wir verwirrt immer weiter hochgedreht haben. Das Menü hätte z-index eine Million haben können. In seiner Kiste hört es von draußen niemand.
Und jetzt die Falle in der Falle. Wir haben den letzten Keyframe auf transform: none geändert, in der Annahme, der gehaltene Wert wäre damit harmlos. Der berechnete Stil meldete weiter eine Matrix. Browser lösen das transform einer angewandten Animation als Matrixwert auf, und die Einheitsmatrix zählt fürs Stapeln immer noch als transform. Der Stapelkontext hat unsere Reparatur überlebt.
/* BAD: the finished animation keeps applying its last keyframe forever. The header remains a stacking context for the life of the page. */.enter { animation: rise 0.5s ease-out both;} /* GOOD: fill backwards only. The element returns to its natural state after the animation, which is identical to the final keyframe anyway. */.enter { animation: rise 0.5s ease-out backwards;} @keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; }}Die Regel, die wir aufgeschrieben haben
Einblendanimationen auf Containern füllen backwards, nie both. Backwards deckt die einzige Lücke ab, auf die es ankommt, nämlich die Frames vor dem Start einer verzögerten Animation. Ist die Animation vorbei, löst sich das Element vollständig von ihr und hört auf, ein Stapelkontext zu sein. Optisch ist das Ergebnis identisch, weil eine gut gebaute Einblendung im natürlichen Zustand des Elements endet.

- Vertrau elementFromPoint mehr als deinen Augen. Die optische Reihenfolge und die Trefferprüfung können sich widersprechen, und nur eine von beiden bekommt den Klick.
- Wenn z-index keinen Sinn mehr ergibt, hör auf, ihn hochzudrehen, und fang an, Stapelkontexte zu jagen. Der Durchlauf über die Vorfahren oben dauert eine Minute.
- Prüf, was deine fertigen Animationen zurücklassen. getComputedStyle auf einer ruhigen Seite sagt dir, was noch angewandt ist.
- Gehaltene transforms sind transforms, auch wenn im Keyframe none steht.
Die Animation dauerte eine halbe Sekunde. Ihre Nebenwirkung hatte gar kein Ende.