---
title: "O menu que se recusava a ser clicado"
description: "Um menu desenhava-se na perfeição, ficava exatamente onde devia, e engolia todos os cliques. O culpado era uma animação de entrada que nunca acabava mesmo. Uma visita curta aos contextos de empilhamento em CSS, aos modos de preenchimento de animação, e a como caçar erros que não se veem."
url: "https://bookatu.com/pt-pt/blog/the-dropdown-that-refused-to-be-clicked"
language: "pt"
section: "Blog"
category: "Engenharia"
author: "A equipa de engenharia do Bookatu"
published: "2026-07-11"
updated: "2026-07-11"
tags: ["contexto de empilhamento css", "animation fill mode", "z-index não funciona", "depuração de frontend", "transform e empilhamento em css"]
---

# O menu que se recusava a ser clicado

Um menu desenhava-se na perfeição, ficava exatamente onde devia, e engolia todos os cliques. O culpado era uma animação de entrada que nunca acabava mesmo. Uma visita curta aos contextos de empilhamento em CSS, aos modos de preenchimento de animação, e a como caçar erros que não se veem.

> **Em resumo**
>
> - animation-fill-mode: both continua a aplicar o último keyframe para sempre, e um transform aplicado torna o elemento um contexto de empilhamento permanente.
> - Isso prende todos os menus dentro do contentor animado: o z-index deles deixa de conseguir competir com irmãos posteriores que tenham os seus próprios contextos de empilhamento.
> - Terminar o keyframe em transform: none não o salva. Os navegadores calculam o valor preenchido como uma matriz identidade, que continua a contar como um transform.
> - As animações de entrada em contentores devem preencher para trás, não em ambos os sentidos. O estado final natural já é igual ao último keyframe.

Lançámos um menu que não podia ser clicado. Abria bem. Desenhava-se no sítio certo, totalmente visível, com o z-index correto e sem erros na consola. Os cliques atravessavam-no e iam parar ao painel de baixo, como se o menu fosse um fantasma. Se alguma vez lutou com um erro destes, já conhece a parte desnorteante: está tudo com bom aspeto, e nada funciona.

## Descobrir quem está mesmo por cima

A primeira resposta honesta veio do navegador, não de ler código. O document.elementFromPoint diz-lhe que elemento recebe mesmo um clique numa coordenada. Apontámo-lo para o centro de um item do menu, e devolveu um cartão que ficava mais à frente na página. O menu estava visualmente por cima e fisicamente por baixo.

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

A travessia encontrou-o. O cabeçalho que continha o menu reportava um transform, apesar de nenhuma folha de estilos lho ter dado. O único transform em toda a volta era uma animação de entrada, um pequeno aparecer e subir que tinha terminado meio segundo depois de a página carregar.

## Os modos de preenchimento sobrevivem às animações

animation-fill-mode: both quer dizer que o elemento mantém o primeiro keyframe antes de a animação começar e fica com o último keyframe depois de ela acabar. Fica, para sempre. Se algum keyframe animar transform, a animação terminada continua a aplicar um transform, e um elemento com transform passa a ser um contexto de empilhamento. O z-index de todos os descendentes fica agora preso lá dentro, a competir só com os irmãos locais, nunca com o resto da página.

A nossa página tinha painéis a seguir ao cabeçalho que usavam backdrop-filter, que cria o seu próprio contexto de empilhamento. Duas caixas seladas, e a ordem de pintura entre elas decidida pela ordem na árvore e não pelo z-index que íamos subindo à toa. O menu podia ter z-index de um milhão. Dentro da caixa dele, ninguém lá fora o ouvia.

E aqui está a armadilha dentro da armadilha. Mudámos o último keyframe para transform: none, a raciocinar que o valor retido ficaria então inofensivo. O estilo calculado continuava a reportar uma matriz. Os navegadores resolvem o transform de uma animação aplicada como um valor de matriz, e a matriz identidade continua a contar como um transform para efeitos de empilhamento. O contexto de empilhamento sobreviveu à nossa correção.

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

## A regra que escrevemos

As animações de entrada em contentores preenchem para trás, nunca em ambos os sentidos. O preenchimento para trás cobre a única lacuna que interessa, os frames antes de uma animação com atraso começar. Assim que a animação acaba, o elemento desliga-se dela por completo e deixa de ser um contexto de empilhamento. O resultado visual é idêntico, porque uma entrada bem feita acaba no estado natural do elemento.

![Um menu a abrir corretamente por cima de painéis de vidro fosco num painel escuro](https://bookatu.com/blog/shots/stock-menu-open-dark.png)

*Depois da correção: o menu pinta por cima dos painéis foscos e, mais a propósito, apanha os próprios cliques.*

- Confie mais no elementFromPoint do que nos seus olhos. A ordem visual e a ordem de deteção de cliques podem discordar, e só uma delas fica com o clique.
- Quando o z-index deixa de fazer sentido, pare de o aumentar e comece a caçar contextos de empilhamento. A travessia dos antepassados acima demora um minuto.
- Audite o que as suas animações terminadas deixam para trás. O getComputedStyle numa página parada diz-lhe o que continua aplicado.
- Transforms preenchidos são transforms, mesmo quando o keyframe diz none.

> A animação durava meio segundo. O efeito secundário dela não tinha hora de fim nenhuma.
