[CSS] Menu Dropdown usando apenas CSS.

Introdução

Hoje venho apresentar um projeto de um menu dropdown (menu que aparece logo abaixo quando um botão é apertado) com apenas CSS na sua construção, usando pseudo-classes no lugar da famosa classe "active".

full-menu-preview-image

O objetivo desse post é desmistificar para programadores Front-End iniciantes (ou não) que muitas vezes tendem a pensar que a solução de certo problema específico é encontrado apenas no script sendo que, muitas vezes a solução já existe no CSS (até com menos dor de cabeça).

O projeto no Codepen pode ser encontrado neste link caso queira apenas passar o olho no código e ver o projeto funcionando, abaixo estará a explicação do projeto. Não aconselho a leitura para pessoas mais experientes em CSS pois a explicação será extensa e ira conter conceitos bem básicos como seletores, pseudo-classes e métodos .

Sumário

Importante freezar que este projeto só é possível graças ao seletor de irmão adjacente, caracterizado pelo sinal de mais (+).

div + a {
  background: red;
}

Este seletor permite selecionar todo elemento que for seguido do elemento passado inicialmente no seletor. No exemplo acima, estamos definindo um fundo vermelho para todo elemento <a> que estiver logo após uma <div>, está ordem sera definida pelo código encontrado no documento HTML.

<div></div>
<a class="home">Home</a>
<section></section>
<a class="about">About</a>

<a> que contém a classe home será selecionado, já o que possúi a classe about não.

O projeto também utiliza pseudo-classes como :active, que permite aplicar estilos a um elemento no momento em que o mesmo está sendo pressionado, outra pseudo-classe utilizada é o :hover que seleciona o elemento no momento em que o ponteiro paira por cima dele.

A syntáxe de uma pseudo-classe é como no exemplo abaixo.

  .Menu:active {
    opacity: 50%;
  }

Menu tera sua opacidade reduzida na metade quando o mesmo for pressionado.

O código HTML

O projeto se inicia apenas com uma <div> que tem papel de segurar todo nosso menu no código, essa <div> recebe a classe de menu.

  <div class="menu"></div>

Dentro do nosso menu teremos dois elementos essenciais que seram eles um <button>, o qual acionara o nosso menu, e um elemento especial que fara sentido mais pra frente na explicação mas no momento, sera apenas uma <div> qualquer que receberá de início seu display como none.

<button> ira receber a classe open-menu e a nossa <div> ira receber menu-trigger.

  <div class="menu">
    <button class="open-menu" type="button">
      Menu
    </button>
    <div class="menu-trigger"></div>
  </div>

Note que menu-trigger está logo após open-menu no código.

E por último mas não menos importante, teremos a navegação do menu que receberá a classe nav-menu, e seus items com a classe nav-menu-item.

  <div class="menu">
    <button class="open-menu" type="button">
      Menu
    </button>
    <div class="menu-trigger"></div>
    <ul class="nav-menu">
      <li class="nav-menu-item">Edit</li>
      <li class="nav-menu-item">Duplicate</li>
      <li class="nav-menu-item">Archive</li>
      <li class="nav-menu-item">Move</li>
      <li class="nav-menu-item">Share</li>
      <li class="nav-menu-item">Add to favorites</li>
      <li class="nav-menu-item">Delete</li>
    </ul>
  </div>

nav-menu também receberá seu display como none para ser ativado posteriormente.

O que temos até agora considerando nossas propiedades de estilo foram aplicadas no CSS é o seguinte.

menu-preview

Apenas nosso <button> open-menu é possível de ser visualizado de inicio.

O código CSS

Para melhor entendimento e também não deixar a explicação mais extensa, não irei citar aqui as propiedades referentes ao estilo do nosso menu, apenas aquelas que forem essenciais para o funcionamento do mesmo.

Nossa <div> menu irá receber de importante apenas uma mudança no seu fluxo de posicionamento que passará a ser relative e não mais static, isso nos ajudara a posicionar outros elementos no futuro.

  .menu {
    position: relative;
  }

Nossa navegação terá de essencial apenas os seguintes estilos:

  .nav-menu {
    opacity: 0;
    position: absolute;
    pointer-events: none;
  }

Esses estilos iram garantir primeiro que nossa navegação não seja visível inicialmente e que não possa ser alvo de eventos de ponteiro (para que o usuário não consiga clicar nos seus items sem a navegação está ativada) e também tera seu fluxo de posicionamento alterado com seu position definido como absolute para garantir que seu conteúdo sobreponha tudo e que seu tamanho não influêncie no posicionamento dos demais elementos na tela.

Agora aqui é onde a mágica começa a acontecer pois, teremos no nosso <button> open-menu, propiedades no seu estado :active que influenciaram no comportamento daquela nossa <div> menu-trigger especial usando o seletor de irmao adjacente do CSS ( + ).

  .open-menu:active + .menu-trigger {
    display: block;
  }

No exemplo acima estamos dizendo que, quando nosso elemento <button> open-menu estiver sendo pressionado, queremos que o próximo elemento a ele receba a propiedade de display definida como block.

Como dito mais acima, sabemos que o próximo elemento a ele é a nossa <div> menu-trigger especial que até então estava com seu display definido como none.

Os estilos essenciais contidos nessa <div> são o seguinte.

  .menu-trigger {
    width: 100%;
    height: 100%;
    opacity: 0;
    display: none;
    position: absolute;
    top: 0;
  }

Esses estilos garantem que <div> menu-trigger se comporte da seguinte maneira quando for ativado.

menu-trigger-preview-off

No momento em que <button> open-menu é pressionado, menu-trigger passa a poder ser visualizado e interagido pelo usuário.

No exemplo acima, menu-trigger pode ser visualizado como um retangulo azul que cobre todo nosso <button> porém, no projeto final iremos utilizar apenas sua habilidade de interação com o usuário, seu estilo final será invisivel (opacity: 0%;) independente se estiver com display none ou block.

Agora, menu-trigger se mantém ativo enquanto o ponteiro estiver pairando por cima dele, mas como isso é possível?

Simples, temos na nossa <div> menu-trigger os seguintes estilos quando o ponteiro está em cima da mesma:

  .menu-trigger:hover {
    display: block;
  }

Então o que acontece aqui é o seguinte, quando o usuário pressiona o <button> open-menu, nossa <div> menu-trigger aparece e sobrepõe o <button> e, já que o ponteiro já estava acima dele, automaticamente os estilos contidos no estado de :hover de menu-trigger é ativado o que faz com que o mesmo se mantenha com display block enquanto o mouse pairar sobre ele.

Lembrando que no projeto em sí a <div> menu-trigger possui sua opacidade zerada porém, sua interação é preservada.

E para completar, o mesmo fluxo de eventos que acontecem com o <button> open-menu e a <div> menu-trigger, acontece com a <ul> nav-menu e menu-trigger.

Quando a <div> menu-trigger é pairada pelo ponteiro, além de aplicar os estilos a ela mesma de display block, também ativa o seguintes código:

  .menu-trigger:hover + .nav-menu {
    pointer-events: auto;
    opacity: 100%;
  }

Que diz que, quando menu-trigger estiver no estado de :hover, então o próximo elemento a ele (que no caso será o nosso menu) terá seus eventos de ponteiro restabelecidos e sua opacidade será de cem porcento.

Mas para que restabelecemos os eventos de ponteiro da nossa navegação?

Simples, na nossa navegação temos estilos específicos que só seram aplicados quando nosso menu estiver no estado de :hover, que no final irá usar a mesma lógica que foi usada para ativar a <div> menu-trigger e manter a mesma ativada.

  .nav-menu:hover {
    pointer-events: initial;
    opacity: 1;
  }

Os eventos de ponteiro sendo restaurados só nesse momento, garante que a navegação só será visível depois que o fluxo de eventos do <button> open-menu e da <div> menu-trigger acontecer e o usuário manter o mouse pairando acima ou da navegação em sí, ou acima de menu-trigger.

Toda essa estrutura resulta no seguinte comportamento:

full-menu-preview

Conclusão

Esse projeto não tem como fim o uso do mesmo em projetos reais, o objetivo aqui é apenas mostrar que não existe solução única para um problema e que não existe ferramente única para resolução do mesmo.

A vontade de fazer esse projeto vem de uma frustação antiga minha de quando eu achava que tudo que era relacionado com interação só era possível usando JavaScript quando na verdade não é bem assim. Existem interações simples que podem sim ser feitas com CSS em projetos reais sem prejudicar a experiência do usuário (UX).

Enfim, obrigado quem chegou até aqui e espero ter contribúido para sua evolução seja lá de qual for a maneira, futuramente irei trazer mais desse tipo de conteúdo, um abraço.

Acredito que a grande questão aqui é a acessibilidade. Eu também já tentei fazer menus dropdown com CSS, mas a acessibilidade sempre fica comprometida.

É sempre bom a gente testar a navegação por teclado, dentre outras coisas, quando vamos criar um componente.

Depois que eu vi esse vídeo aqui, fiquei impressionado com a quantidade de detalhes que precisam ser considerados ao criar um dropdown. https://www.youtube.com/watch?v=pcMYcjtWwVI

Entendo como **acessibilidade** é importante, ainda mais no dia de hoje com tantas de pessoas com acesso a internet e seria um erro eu dizer que o projeto acima é reutilizável em um projeto de larga escala, aliás eu digo o contrário bem aqui. > Esse projeto não tem como fim o uso do mesmo em projetos reais. O objetivo mesmo é mostrar que com um pouco de **criatividade** podemos criar coisas **complexas** a partir de coisas mais **simples**, essa mesma **criatividade** que caso você demonstre ter em uma possível entrevista de emprego, pode te colocar um passo a frente de outros canditados, existem muito objetivos com esse post, porém e infelizmente **acessibilidade** não é um deles. Vou procurar saber mais sobre **semântica** nesse tipo de **feature** e futuramente trazer mais conteúdo sobre, de forma a abranger tudo que for relacionado ao assunto, obrigado pela observação.

Muito bom! Uma dúvida que fiquei por que você quis usar o nav em vez de ul com li? Fico pensando que talvez não seja muito apropriado usar tag semantica de navegação pra dropdown... Geralmente dropdown é pra ações.

Realmente, utilizei `nav` por que na hora estava mais focado no funcionamento do que na semântica mesmo, vou atualizar para não confundi a galera, obrigado pela observação.

Parabéns!

Simples e funcional.

Fico pensando até que ponto precisamos utilizar determinados frameworks se com o avanço que o HTML e CSS tiveram com poucos códigos desenvolvemos "componentes nativamente", digo no sentido de utilizar o mínimo de código necessário, limpo e organizado.

O uso de **frameworks** sempre foram relacionados a duas principais **dores** de desenvolvedores, a primeira sendo a **compatibilidade**, exemplos de soluções para essas dores seriam como **jQuery**, **Webpack** e **Babel**, e a outra dor seria **economização de tempo** como fazem o **Bootstrap**, **Bulma** e o **Tailwind**. Realmente muitos desses **frameworks** estão e vão cair em desuso por conta da evolução de suas **tecnologias** nativas, **jQuery** hoje em dia só se ver em projetos que tem como alvo muitos tipos de público e necessitam que seus alcances não sejam prejudicados por incompatibilidade de código. Futuramente os únicos que iram "sobreviver" ao meu ver com essa rápida **evolução** que está ocorrendo na web seram aqueles que recebem manuntenção de grandes empresas de **tecnologia** como por exemplo o **React**, que é mantida hoje pela empresa do Facebook, a Meta.

Muito bom Reinaldo!

Conteúdo muito bom, eu realmente estava precisando de algo como isso.

Eu adoro nos meus projetos trabalhar com CSS ou SASS por que gosto de fazer as coisas do meu jeito, a utilização das bibliotecas como tailwind ou bootstrap te limita aquilo a qual ela é destinada, post muito bom!!

Muito bem explicado, gostei demais. Se der faz mais posts assim, talvez algum projeto ao algo do tipo.

Ótimo tutorial, eu sempre tento usar o menos possivel de JavaScript, pra ter um código mais rápido e limpo.

muito legal, me lembrou o windows 11

Mano muito bem explicado, estava querendo fazer algo exatamente nesse estilo pro redesign do meu portfolio. Com certeza usarei as explicações e exemplos do que aprendi aqui. Obrigado! :)

sou iniciante no front, e consegui compreender tudo perfeitamente, obrigado 😁

Codepen é muito bom.