---
title: "Rozwijane menu, które nie chciało dać się kliknąć"
description: "Menu renderowało się bez zarzutu, siedziało dokładnie tam, gdzie powinno, i połykało każde kliknięcie. Winowajcą była animacja wejścia, która nigdy naprawdę się nie kończyła. Krótka wycieczka po kontekstach nakładania w CSS, trybach wypełniania animacji i polowaniu na błędy, których nie widać."
url: "https://bookatu.com/pl-pl/blog/the-dropdown-that-refused-to-be-clicked"
language: "pl"
section: "Blog"
category: "Technologia"
author: "Zespół inżynierów Bookatu"
published: "2026-07-11"
updated: "2026-07-11"
tags: ["kontekst nakładania css", "animation-fill-mode", "z-index nie działa", "debugowanie frontendu", "css transform a nakładanie"]
---

# Rozwijane menu, które nie chciało dać się kliknąć

Menu renderowało się bez zarzutu, siedziało dokładnie tam, gdzie powinno, i połykało każde kliknięcie. Winowajcą była animacja wejścia, która nigdy naprawdę się nie kończyła. Krótka wycieczka po kontekstach nakładania w CSS, trybach wypełniania animacji i polowaniu na błędy, których nie widać.

> **W skrócie**
>
> - animation-fill-mode: both bez końca stosuje ostatnią klatkę kluczową, a zastosowany transform robi z elementu trwały kontekst nakładania.
> - To zamyka każde rozwijane menu wewnątrz animowanego kontenera: jego z-index nie może już konkurować z późniejszym rodzeństwem, które ma własne konteksty nakładania.
> - Zakończenie klatki na transform: none Cię nie ratuje. Przeglądarki wyliczają wypełnioną wartość jako macierz jednostkową, a ta wciąż liczy się jako transform.
> - Animacje wejścia na kontenerach powinny wypełniać wstecz, a nie w obie strony. Naturalny stan końcowy i tak jest równy ostatniej klatce.

Wypuściliśmy rozwijane menu, którego nie dało się kliknąć. Otwierało się bez problemu. Renderowało się we właściwym miejscu, w pełni widoczne, z poprawnym z-indexem, bez błędów w konsoli. Kliknięcia przelatywały przez nie prosto do panelu pod spodem, jakby menu było duchem. Jeśli kiedykolwiek walczyłeś z takim błędem, znasz już tę dezorientującą część: wszystko wygląda dobrze i nic nie działa.

## Ustalanie, kto naprawdę jest na wierzchu

Pierwsza uczciwa odpowiedź przyszła od przeglądarki, a nie z czytania kodu. document.elementFromPoint mówi, który element naprawdę odbiera kliknięcie w danym punkcie. Wycelowaliśmy w środek pozycji menu, a on zwrócił kartę leżącą dalej na stronie. Menu było wizualnie na wierzchu i fizycznie pod spodem.

```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 context
for (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);
}
```

Przejście po przodkach to znalazło. Nagłówek zawierający menu raportował transform, choć żaden arkusz stylów mu go nie dawał. Jedynym transformem gdziekolwiek w pobliżu była animacja wejścia, drobne pojawienie się z uniesieniem, które skończyło się pół sekundy po wczytaniu strony.

## Tryby wypełniania przeżywają swoje animacje

animation-fill-mode: both znaczy, że element trzyma pierwszą klatkę kluczową przed startem animacji i zachowuje ostatnią po jej końcu. Zachowuje, na zawsze. Jeśli którakolwiek klatka animuje transform, skończona animacja nadal stosuje transform, a element z transformem staje się kontekstem nakładania. Z-index każdego potomka jest odtąd uwięziony w środku i konkuruje wyłącznie z lokalnym rodzeństwem, nigdy z resztą strony.

Na naszej stronie za nagłówkiem stały panele używające backdrop-filter, który tworzy własny kontekst nakładania. Dwa zaplombowane pudełka, a kolejność malowania między nimi rozstrzygana przez kolejność w drzewie, a nie przez z-index, który w zdumieniu podnosiliśmy coraz wyżej. Menu mogło mieć z-index równy milionowi. Wewnątrz swojego pudełka nikt z zewnątrz go nie słyszał.

A oto pułapka wewnątrz pułapki. Zmieniliśmy ostatnią klatkę kluczową na transform: none, uznając, że zachowana wartość będzie wtedy nieszkodliwa. Wyliczony styl nadal raportował macierz. Przeglądarki rozwiązują transform zastosowanej animacji jako wartość macierzową, a macierz jednostkowa nadal liczy się jako transform w kwestii nakładania. Kontekst nakładania przeżył naszą poprawkę.

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

## Reguła, którą zapisaliśmy

Animacje wejścia na kontenerach wypełniają wstecz, nigdy w obie strony. Wstecz pokrywa jedyną lukę, która ma znaczenie, czyli klatki przed startem opóźnionej animacji. Gdy animacja się kończy, element całkowicie się od niej odczepia i przestaje być kontekstem nakładania. Efekt wizualny jest identyczny, bo dobrze zbudowane wejście kończy się w naturalnym stanie elementu.

![Rozwijane menu otwierające się poprawnie nad matowymi szklanymi panelami w ciemnym panelu](https://bookatu.com/blog/shots/stock-menu-open-dark.png)

*Po poprawce: menu maluje się nad matowymi panelami i, co ważniejsze, łapie własne kliknięcia.*

- Ufaj elementFromPoint bardziej niż własnym oczom. Kolejność wizualna i kolejność trafień mogą się nie zgadzać, a kliknięcie bierze tylko jedna z nich.
- Gdy z-index przestaje mieć sens, przestań go podnosić i zacznij polować na konteksty nakładania. Przejście po przodkach z powyższego kodu zajmuje minutę.
- Sprawdzaj, co zostawiają po sobie skończone animacje. getComputedStyle na spokojnej stronie mówi, co jest wciąż zastosowane.
- Wypełnione transformy to transformy, nawet gdy klatka mówi none.

> Animacja trwała pół sekundy. Jej skutek uboczny nie miał czasu zakończenia w ogóle.
