Como criar um componente de navegação lateral

Uma visão geral básica de como criar uma navegação lateral deslizante responsiva

Nesta postagem, quero compartilhar como prototipei um componente de navegação lateral para a Web que é responsivo, com estado, compatível com navegação pelo teclado, funciona com e sem JavaScript e funciona em vários navegadores. Confira a demonstração.

Se você preferir o vídeo, aqui está uma versão do YouTube desta postagem:

Visão geral

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

Demonstração de layout responsivo de computador para dispositivo móvel
Redução dos temas claro e escuro no iOS e no Android

Táticas da Web

Nesta análise detalhada de componentes, tive a satisfação de combinar alguns recursos essenciais da plataforma da Web:

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

Minha solução tem uma barra lateral e alterna somente quando está em uma janela de visualização "dispositivo móvel" de 540px ou menos. O 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 CSS :target

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>

Ao clicar em cada um desses links, o estado de hash do URL da página é alterado. Em seguida, com uma pseudoclasse, vou mostrar e ocultar a 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 navegação lateral de posição absoluta ou fixa. No entanto, a grade, com a sintaxe grid-area, permite atribuir vários elementos à mesma linha ou coluna.

As pilhas

O elemento de layout principal #sidenav-container é uma grade que cria uma linha e duas colunas, uma de cada uma delas denominada stack. Quando o espaço é restrito, o CSS atribui todos os filhos do elemento <main> ao mesmo nome de grade, colocando todos os elementos no mesmo espaço, 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. Ele tem dois filhos: o contêiner de navegação <nav> chamado [nav] e um pano 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 "Fechar" do espaço negativo.

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

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

Agora, nosso layout está empilhado no tamanho da janela de visualização de dispositivos móveis. Até que eu adicione alguns estilos novos, o artigo vai se sobrepor por padrão. Aqui está uma UX que estou buscando na próxima seção:

  • Animar para abrir e fechar
  • A animação só precisa ser feita se o usuário concordar com o movimento
  • Animar visibility para que o foco do teclado não entre no elemento fora da tela

À medida que começo a implementar animações de movimento, quero começar pensando na 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 do sistema operacional de um usuário pelo 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 navegação lateral está deslizando e fechando, se um usuário preferir o movimento reduzido, eu movo o elemento instantaneamente para a visualização, mantendo o estado sem movimento.

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

Saída da navegação lateral (padrão)

Para definir o estado padrão da navegação lateral em dispositivos móveis como um estado fora da tela, posicione 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 navegação lateral não apareça na janela de visualização principal quando ela estiver oculta.

@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);
  }
}
Navegação lateral em

Quando o elemento #sidenav corresponder a :target, defina a posição translateX() como a base inicial 0 e observe como o CSS deslizar o elemento da posição de saída de -110vw para a posição "in" de 0 em var(--duration) quando o hash do URL for modificado.

@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 desativado, para que os sistemas não coloquem o foco em um menu fora da tela. Faço isso ao definir uma transição de visibilidade quando o :target muda.

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

Melhorias de acessibilidade na UX

Essa solução depende da alteração do URL para que o estado seja gerenciado. Naturalmente, o elemento <a> precisa ser usado aqui, e ele recebe alguns bons recursos de acessibilidade sem custo financeiro. Vamos enfeitar nossos elementos interativos com rótulos que expressam 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 UX de interação com o teclado e narração.

Agora, nossos botões de interação principais declaram claramente a intenção deles para mouse e teclado.

:is(:hover, :focus)

Esse pseudosseletor funcional CSS útil permite manter a inclusão rapidamente com nossos estilos de passar o cursor, compartilhando-os também com foco.

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

Borrifar no JavaScript

Pressione escape para fechar

A tecla Escape do teclado vai fechar o menu corretamente? 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 abertura e fechamento empilhe várias entradas no histórico do navegador, adicione o seguinte JavaScript inline ao botão "Fechar":

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

Isso vai remover a entrada do histórico de URLs no fechamento, fazendo com que o menu nunca tenha sido aberto.

Foco na UX

O próximo snippet nos ajuda a colocar o foco nos botões de abrir e fechar depois que eles abrem ou fecham. 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 "Fechar". Quando a navegação lateral é fechada, foque no botão "Abrir". Para fazer isso, chame focus() no elemento em JavaScript.

Conclusão

Agora que você sabe como eu fiz isso, como faria?! Isso deixa 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, envie um tweet para sua versão e eu a adicionarei à seção Remixes da comunidade abaixo.

Remixes da comunidade