Como criar um componente de navegação lateral

Uma visão geral básica de como criar um sidenav deslizante responsivo

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

Se preferir vídeos, confira a 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 vão ter computadores poderosos e outros vão acessar o site em um dispositivo móvel pequeno. Todas as pessoas que visitarem o site devem poder abrir e fechar o menu.

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

Web Tactics

Nesta análise de componentes, tive a alegria de combinar alguns recursos essenciais da plataforma da Web:

  1. CSS :target
  2. grade 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 de UX do focus

Minha solução tem uma barra lateral e alterna apenas quando está em uma janela de visualização "mobile" de 540px ou menos. 540px será o 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 do hash do URL da página. Em seguida, com uma pseudoclasse, mostro e oculto o sidenav:

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

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

Grade CSS

No passado, eu só usava layouts e componentes de navegação lateral em posição absoluta ou fixa. No entanto, 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, uma de cada uma é 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, 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 duas filhas: o contêiner de navegação <nav> com o nome [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 que você quer para a sobreposição de 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

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

  • Animar a abertura e o fechamento
  • Só animar com movimento se o usuário permitir
  • 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 começar com a acessibilidade em mente.

Movimento acessível

Nem todo mundo quer uma experiência de movimento deslizante. 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 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 a duração aplicada.

Agora, quando o sidenav desliza para abrir e fechar, se um usuário preferir um movimento reduzido, eu moverei o elemento para a visualização instantaneamente, mantendo o estado sem movimento.

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

Sidenav para fora (padrão)

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

Adicionei outro 10vw ao código típico fora da tela de -100vw, para garantir que o box-shadow do menu lateral não apareça na viewport 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, e observe como o CSS desliza o elemento da posição de -110vw para a posição de 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 da tela, para que os sistemas não coloquem o foco em um menu fora da tela. Para fazer isso, configure uma transição de visibilidade quando o :target mudar.

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

Aprimoramentos na UX de acessibilidade

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

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

:is(:hover, :focus)

Esse pseudo-seletor funcional CSS útil nos permite ser rapidamente inclusivos com nossos estilos de passar o cursor, compartilhando-os também com o foco.

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

JavaScript básico

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 abertura e fechamento adicione várias entradas ao histórico do navegador, adicione o seguinte JavaScript inline ao botão de fechamento:

<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, fazendo com que pareça que o menu nunca foi aberto.

Focus UX

O snippet a seguir nos ajuda a colocar o foco nos botões de abrir e fechar depois de abrir ou fechar. 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 o sidenav abrir, foque o botão de fechar. Quando o sidenav fechar, foque o botão aberto. Para isso, chamo focus() no elemento em JavaScript.

Conclusão

Agora que você sabe como eu fiz, 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, envie um tweet com sua versão e vou adicioná-la à seção Remixes da comunidade abaixo.

Remixes da comunidade