Como criar um componente de navegação lateral

Uma visão geral básica de como criar uma sidenav responsiva que desliza para fora

Nesta postagem, quero compartilhar com você como criei um protótipo de um componente Sidenav para a Web que é responsivo, com estado, compatível com navegação por teclado, funciona com e sem JavaScript e em todos os navegadores. Teste a demonstração.

Se preferir vídeo, confira uma versão desta postagem no YouTube:

Visão geral

É difícil criar um sistema de navegação responsivo. Alguns usuários vão usar um teclado, outros terão computadores potentes e outros vão acessar de um pequeno dispositivo móvel. Todos os visitantes precisam conseguir abrir e fechar o menu.

Demonstração de layout responsivo de computador para dispositivos móveis
Temas claro e escuro no iOS e Android

Táticas da Web

Nesta exploração de componentes, tive a alegria de combinar alguns recursos essenciais da plataforma da Web:

  1. CSS :target
  2. grid do CSS
  3. Transformações do CSS
  4. Consultas de mídia CSS para janela de visualização e preferência do usuário
  5. JS para melhorias na UX do focus

Minha solução tem uma barra lateral e só alterna quando está em uma janela de visualização "móvel" de 540px ou menos. 540px será nosso ponto de interrupção para alternar entre o layout interativo para dispositivos móveis e o layout estático para computadores.

Pseudoclasse :target do CSS

Um link <a> define o hash do URL como #sidenav-open e o outro como vazio (''). Por fim, um elemento tem o id para corresponder ao hash:

<a href="#sidenav-open" id="sidenav-button" title="Open Menu" aria-label="Open Menu">

<a href="#" id="sidenav-close" title="Close Menu" aria-label="Close Menu"></a>

<aside id="sidenav-open">
  …
</aside>

Clicar em cada um desses links muda o estado hash do URL da página. Em seguida, com uma pseudoclasse, mostro e oculto a barra de navegação lateral:

@media (max-width: 540px) {
  #sidenav-open {
    visibility: hidden;
  }

  #sidenav-open:target {
    visibility: visible;
  }
}

Grade CSS

Antes, eu só usava layouts e componentes de barra lateral de posição absoluta ou fixa. A grade, com a sintaxe grid-area, permite atribuir vários elementos à mesma linha ou coluna.

Pilhas

O elemento de layout principal #sidenav-container é uma grade que cria uma linha e duas colunas, sendo uma de cada chamada stack. Quando o espaço é limitado, o CSS atribui todos os filhos do elemento <main> ao mesmo nome de grade, colocando todos os elementos no mesmo espaço e criando uma pilha.

#sidenav-container {
  display: grid;
  grid: [stack] 1fr / min-content [stack] 1fr;
  min-height: 100vh;
}

@media (max-width: 540px) {
  #sidenav-container > * {
    grid-area: stack;
  }
}

O <aside> é o elemento de animação que contém a navegação lateral. Ela tem dois filhos: o contêiner de navegação <nav> chamado [nav] e um plano de fundo <a> chamado [escape], que é usado para fechar o menu.

#sidenav-open {
  display: grid;
  grid-template-columns: [nav] 2fr [escape] 1fr;
}

Ajuste 2fr e 1fr para encontrar a proporção desejada para a sobreposição do menu e o botão de fechar do espaço negativo.

Uma demonstração do que acontece quando você muda a proporção.

Transformações e transições 3D do CSS

Nosso layout agora está empilhado em um tamanho de janela de visualização para dispositivos móveis. Até que eu adicione alguns novos estilos, ele vai sobrepor nosso artigo por padrão. Confira algumas experiências do usuário que quero criar na próxima seção:

  • Animar abrir e fechar
  • Só use animação com movimento se o usuário concordar com isso
  • Animar visibility para que o foco do teclado não entre no elemento fora da tela

Ao começar a implementar animações de movimento, quero priorizar a acessibilidade.

Movimento acessível

Nem todo mundo vai querer uma experiência de movimento de deslizar para fora. Na nossa solução, essa preferência é aplicada ajustando uma variável CSS --duration dentro de uma consulta de mídia. Esse valor de consulta de mídia representa a preferência de um usuário pelo sistema operacional para movimento (se disponível).

#sidenav-open {
  --duration: .6s;
}

@media (prefers-reduced-motion: reduce) {
  #sidenav-open {
    --duration: 1ms;
  }
}
Uma demonstração da interação com e sem duração aplicada.

Agora, quando a barra lateral está abrindo e fechando, se um usuário preferir movimento reduzido, eu movo instantaneamente o elemento para a visualização, mantendo o estado sem movimento.

Transição, transformação, tradução

Sidenav out (padrão)

Para definir o estado padrão da barra de navegação lateral em dispositivos móveis como fora da tela, posiciono o elemento com transform: translateX(-110vw).

Adicionei outro 10vw ao código fora da tela típico de -100vw, para garantir que o box-shadow da barra de navegação lateral não apareça na janela de visualização principal quando estiver oculto.

@media (max-width: 540px) {
  #sidenav-open {
    visibility: hidden;
    transform: translateX(-110vw);
    will-change: transform;
    transition:
      transform var(--duration) var(--easeOutExpo),
      visibility 0s linear var(--duration);
  }
}
Sidenav in

Quando o elemento #sidenav corresponder a :target, defina a posição translateX() como 0 da base inicial e observe como o CSS desliza o elemento da posição externa -110vw para a posição "interna" 0 em var(--duration) quando o hash do URL é alterado.

@media (max-width: 540px) {
  #sidenav-open:target {
    visibility: visible;
    transform: translateX(0);
    transition:
      transform var(--duration) var(--easeOutExpo);
  }
}

Visibilidade da transição

O objetivo agora é ocultar o menu dos leitores de tela quando ele estiver fora, para que os sistemas não foquem em um menu fora da tela. Para isso, defino uma transição de visibilidade quando o :target muda.

  • Ao entrar, não faça a transição da visibilidade. Fique visível imediatamente para que eu possa ver o elemento deslizar e aceitar o foco.
  • Ao sair, faça a transição da visibilidade, mas atrase-a para que ela mude para hidden no final da transição de saída.

Melhorias na UX de acessibilidade

Essa solução depende da mudança do URL para que o estado seja gerenciado. Naturalmente, o elemento <a> deve ser usado aqui, e ele recebe alguns recursos de acessibilidade interessantes sem custos financeiros. Vamos adornar nossos elementos interativos com rótulos que articulam claramente a intenção.

<a href="#" id="sidenav-close" title="Close Menu" aria-label="Close Menu"></a>

<a href="#sidenav-open" id="sidenav-button" class="hamburger" title="Open Menu" aria-label="Open Menu">
  <svg>...</svg>
</a>
Uma demonstração da experiência do usuário de narração e interação com o teclado.

Agora, os botões de interação principais indicam claramente a intenção para mouse e teclado.

:is(:hover, :focus)

Esse pseudoseletor funcional do CSS permite que sejamos inclusivos rapidamente com nossos estilos de passar o cursor, compartilhando-os também com o foco.

.hamburger:is(:hover, :focus) svg > line {
  stroke: hsl(var(--brandHSL));
}

Adicionar JavaScript

Pressione escape para fechar

A tecla Escape no teclado fecha o menu, certo? Vamos conectar isso.

const sidenav = document.querySelector('#sidenav-open');

sidenav.addEventListener('keyup', event => {
  if (event.code === 'Escape') document.location.hash = '';
});
Histórico do navegador

Para evitar que a interação de abrir e fechar acumule várias entradas no histórico do navegador, adicione o seguinte JavaScript inline ao botão de fechar:

<a href="#" id="sidenav-close" title="Close Menu" aria-label="Close Menu" onchange="history.go(-1)"></a>

Isso remove a entrada do histórico de URL ao fechar, como se o menu nunca tivesse sido aberto.

Foco na UX

O snippet a seguir ajuda a focar nos botões de abrir e fechar depois que eles são abertos ou fechados. Quero facilitar a alternância.

sidenav.addEventListener('transitionend', e => {
  const isOpen = document.location.hash === '#sidenav-open';

  isOpen
      ? document.querySelector('#sidenav-close').focus()
      : document.querySelector('#sidenav-button').focus();
})

Quando a navegação lateral abrir, foque o botão de fechar. Quando a navegação lateral for fechada, foque o botão de abrir. Para isso, chamo focus() no elemento em JavaScript.

Conclusão

Agora que você sabe como eu fiz isso, como você faria? Isso cria uma arquitetura de componentes divertida. Quem vai criar a primeira versão com slots? 🙂

Vamos diversificar nossas abordagens e aprender todas as maneiras de criar na Web. Crie um Glitch, me envie um tweet com sua versão, e eu vou adicioná-la à seção Remixes da comunidade abaixo.

Remixes da comunidade