Journal
Teknik3 min läsning

Menyn som vägrade låta sig klickas

En meny renderades perfekt, låg exakt där den skulle och slukade varje klick. Boven var en entréanimation som aldrig riktigt tog slut. En kort rundtur bland CSS stacking contexts, animation fill modes och hur man jagar buggar man inte kan se.

BTBookatus teknikteam

Vi släppte en rullgardinsmeny som inte gick att klicka på. Den öppnades fint. Den renderades på rätt plats, fullt synlig, rätt z-index, inga fel i konsolen. Klicken gick rakt igenom den ner i panelen under, som om menyn vore ett spöke. Har du någon gång slagits mot en sådan bugg vet du redan vad som är det förvirrande: allt ser rätt ut, och ingenting fungerar.

Att ta reda på vem som faktiskt ligger överst

Det första ärliga svaret kom från webbläsaren, inte från att läsa kod. document.elementFromPoint talar om vilket element som verkligen tar emot ett klick på en viss punkt. Vi riktade den mot mitten av ett menyval, och den returnerade ett kort som låg senare på sidan. Menyn låg visuellt överst och fysiskt underst.

js
// 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);}

Vandringen hittade det. Sidhuvudet som innehöll menyn rapporterade en transform, trots att ingen stilmall gav den någon. Den enda transform som fanns i närheten var en entréanimation, en liten toning uppåt som hade tagit slut en halv sekund efter att sidan laddats.

Fill modes lever längre än sina animationer

animation-fill-mode: both betyder att elementet håller kvar den första keyframen innan animationen börjar och behåller den sista keyframen efter att den slutat. Behåller, för alltid. Om någon keyframe animerar transform tillämpar den avslutade animationen fortfarande en transform, och ett transformerat element blir en stacking context. Varje ättlings z-index är nu inlåst där och tävlar bara med lokala syskon, aldrig med resten av sidan.

Vår sida hade paneler efter sidhuvudet som använde backdrop-filter, vilket skapar en egen stacking context. Två förseglade lådor, och målningsordningen mellan dem avgjord av ordningen i trädet, inte av det z-index vi förvirrat fortsatte höja. Menyn kunde ha haft z-index på en miljon. Inuti sin låda hörde ingen utanför den.

Här är fällan inuti fällan. Vi ändrade den sista keyframen till transform: none, med tanken att det kvarhållna värdet då skulle vara ofarligt. Den beräknade stilen rapporterade fortfarande en matris. Webbläsare löser upp en tillämpad animations transform till ett matrisvärde, och en identitetsmatris räknas fortfarande som en transform när stacking avgörs. Vår stacking context överlevde fixen.

css
/* 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; }}

Regeln vi skrev ner

Entréanimationer på behållare fyller backwards, aldrig both. Backwards täcker den enda lucka som spelar roll, bildrutorna innan en fördröjd animation startar. När animationen tar slut lossnar elementet helt från den och slutar vara en stacking context. Resultatet ser likadant ut, för en välbyggd entré slutar i elementets naturliga läge.

En rullgardinsmeny som öppnas korrekt ovanför frostade glaspaneler i en mörk översikt
Efter fixen: menyn målas ovanför de frostade panelerna och, viktigare, fångar sina egna klick.
  • Lita på elementFromPoint framför dina ögon. Den visuella ordningen och träffordningen kan säga emot varandra, och bara den ena tar emot klicket.
  • När z-index slutar gå ihop, sluta höja det och börja jaga stacking contexts. Vandringen uppåt bland förfäderna tar en minut.
  • Granska vad dina avslutade animationer lämnar efter sig. getComputedStyle på en stilla sida visar vad som fortfarande är tillämpat.
  • Ifyllda transforms är transforms, även när keyframen säger none.

Animationen var en halv sekund lång. Dess sidoeffekt hade ingen sluttid alls.

css stacking contextanimation fill modez-index fungerar intefelsökning frontendcss transform stacking
Vill du sätta det här i verket?

Bookatu ger dig en bokningssida i din egen stil, handpenning, medlemskap, presentkort och påminnelser, med 0% provision på dina bokningar.

Börja gratis