Verhalen
Techniek3 min lezen

Het uitklapmenu dat weigerde aangeklikt te worden

Een menu tekende perfect, stond precies waar het hoorde, en slikte elke klik op. De boosdoener was een openingsanimatie die nooit echt eindigde. Een korte rondleiding langs stacking contexts in CSS, fill modes van animaties, en hoe je bugs opspoort die je niet kunt zien.

HTHet technische team van Bookatu

We leverden een uitklapmenu dat niet aangeklikt kon worden. Het opende prima. Het tekende op de goede plek, volledig zichtbaar, met de juiste z-index en zonder fouten in de console. Klikken gingen er dwars doorheen, het paneel eronder in, alsof het menu een geest was. Heb je ooit met zo'n bug gevochten, dan ken je het verwarrende deel al: alles ziet er goed uit, en niets werkt.

Uitzoeken wie er echt bovenop ligt

Het eerste eerlijke antwoord kwam van de browser en niet van code lezen. document.elementFromPoint vertelt je welk element op een coördinaat echt de klik krijgt. We richtten het op het midden van een menu-item, en het gaf een kaart terug die verderop in de pagina stond. Het menu lag visueel bovenop en fysiek eronder.

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

De wandeling langs de voorouders vond het. De header waar het menu in zat rapporteerde een transform, terwijl geen enkele stylesheet er een gaf. De enige transform in de buurt was een openingsanimatie, een klein fade-and-rise dat een halve seconde na het laden van de pagina klaar was.

Fill modes overleven hun animatie

animation-fill-mode: both betekent dat het element het eerste keyframe vasthoudt voordat de animatie begint en het laatste keyframe vasthoudt nadat ze afgelopen is. Vasthoudt, voorgoed. Animeert een van de keyframes transform, dan past de afgeronde animatie nog steeds een transform toe, en een element met een transform wordt een stacking context. De z-index van elke afstammeling zit daar nu in gevangen en concurreert alleen nog met de buren binnen die doos, nooit met de rest van de pagina.

Onze pagina had na de header panelen die backdrop-filter gebruikten, en dat maakt een eigen stacking context. Twee dichtgeplakte dozen, en de tekenvolgorde ertussen wordt bepaald door de volgorde in de boom en niet door de z-index die wij verward bleven ophogen. Het menu had een z-index van een miljoen kunnen hebben. Binnen zijn eigen doos kon niemand van buiten het horen.

Hier zit de val in de val. We veranderden het laatste keyframe in transform: none, met de redenering dat de vastgehouden waarde dan onschadelijk zou zijn. De berekende stijl rapporteerde nog steeds een matrix. Browsers lossen de transform van een toegepaste animatie op als een matrixwaarde, en een identiteitsmatrix telt voor stacking nog steeds als een transform. De stacking context overleefde onze fix.

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; }}

De regel die we hebben opgeschreven

Openingsanimaties op containers vullen backwards, nooit both. Backwards dekt het enige gat dat ertoe doet, de frames voordat een uitgestelde animatie begint. Zodra de animatie klaar is, maakt het element zich er volledig van los en houdt het op een stacking context te zijn. Het resultaat op het scherm is identiek, want een goed gebouwde opening eindigt in de natuurlijke toestand van het element.

Een uitklapmenu dat correct opent boven matglazen panelen in een donker dashboard
Na de fix: het menu tekent boven de matglazen panelen en, belangrijker nog, vangt zijn eigen klikken op.
  • Vertrouw elementFromPoint boven je ogen. De volgorde op het scherm en de volgorde bij hit-testing kunnen het oneens zijn, en maar één van de twee krijgt de klik.
  • Slaat z-index nergens meer op, hoog hem dan niet verder op maar ga stacking contexts jagen. De wandeling langs de voorouders hierboven kost een minuut.
  • Controleer wat je afgeronde animaties achterlaten. getComputedStyle op een stille pagina vertelt je wat er nog wordt toegepast.
  • Vastgehouden transforms zijn transforms, ook als het keyframe none zegt.

De animatie duurde een halve seconde. Het neveneffect ervan had helemaal geen eindtijd.

css stacking contextanimation fill modez-index werkt nietfrontend debuggencss transform en stacking
Klaar om hiermee aan de slag te gaan?

Bookatu geeft je een boekingspagina in je eigen huisstijl, met aanbetalingen, lidmaatschappen, cadeaubonnen en herinneringen, en 0% commissie op je boekingen.

Gratis beginnen