Como criar um componente de navegação estrutural

Uma visão geral básica de como criar um componente de navegação estrutural responsivo e acessível para que os usuários naveguem pelo site.

Nesta postagem, quero compartilhar ideias sobre uma maneira de criar componentes da navegação estrutural. Teste a demonstração.

Demonstração

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

Visão geral

Um componente de navegação estrutural mostra em que parte da hierarquia do site o usuário está. O nome é de Hansel e Gretel, que colocaram as trilhas de navegação atrás deles em uma floresta escura e conseguiram encontrar o caminho para casa traçando as migalhas para trás.

A navegação estrutural nesta postagem não é padrão, mas sim. Elas oferecem mais funcionalidades ao colocar páginas irmãs diretamente na navegação com um <select>, possibilitando o acesso em várias camadas.

UX em segundo plano

No vídeo de demonstração do componente acima, as categorias de marcador de posição são gêneros de videogames. Essa trilha é criada pelo seguinte caminho: home » rpg » indie » on sale, conforme mostrado abaixo.

Esse componente precisa permitir que os usuários naveguem por essa hierarquia de informações, pulando ramificações e selecionando páginas com velocidade e precisão.

Arquitetura de informações

Acho útil pensar em termos de coleções e itens.

Coleções

Uma coleção é uma matriz de opções para você escolher. Na página inicial do protótipo de navegação estrutural desta postagem, as coleções são FPS, RPG, brawler, rastreador de masmorras, esportes e quebra-cabeças.

Items

Um videogame é um item. Uma coleção específica também pode ser um item se representar outra coleção. Por exemplo, RPG é um item e uma coleção válida. Quando é um item, o usuário acessa a página da coleção. Por exemplo, eles estão na página "RPG", que mostra uma lista de jogos desse tipo, incluindo as subcategorias adicionais "AAA", "Indie" e "Autopublicação".

Em termos de ciência da computação, esse componente de navegação estrutural representa uma matriz multidimensional:

const rawBreadcrumbData = {
  "FPS": {...},
  "RPG": {
    "AAA": {...},
    "indie": {
      "new": {...},
      "on sale": {...},
      "under 5": {...},
    },
    "self published": {...},
  },
  "brawler": {...},
  "dungeon crawler": {...},
  "sports": {...},
  "puzzle": {...},
}

Seu app ou site vai ter uma arquitetura da informação (AI) personalizada criando uma matriz multidimensional diferente, mas espero que o conceito de páginas de destino de coleta e travessia de hierarquia também possa ser usado na sua navegação estrutural.

Layouts

Marcação

Bons componentes começam com o HTML apropriado. Na próxima seção, vou abordar minhas escolhas de marcação e como elas afetam o componente geral.

Esquema claro e escuro

<meta name="color-scheme" content="dark light">

A metatag color-scheme no snippet acima informa ao navegador que a página precisa dos estilos claro e escuro do navegador. A navegação estrutural do exemplo não inclui CSS para esses esquemas de cores e, portanto, usará as cores padrão fornecidas pelo navegador.

<nav class="breadcrumbs" role="navigation"></nav>

É apropriado usar o elemento <nav> para navegação no site, que tem uma função de navegação ARIA implícita. Nos testes, notei que, depois que o atributo role mudou a forma como um leitor de tela interagiu com o elemento, ele foi anunciado como navegação. Por isso, optei por adicioná-lo.

Ícones

Quando um ícone é repetido em uma página, o elemento SVG <use> significa que você pode definir o path uma vez e usá-lo para todas as instâncias do ícone. Isso impede que as mesmas informações de caminho sejam repetidas, causando documentos maiores e o potencial de inconsistência de caminho.

Para usar essa técnica, adicione um elemento SVG oculto à página e envolva os ícones em um elemento <symbol> com um ID exclusivo:

<svg style="display: none;">

  <symbol id="icon-home">
    <title>A home icon</title>
    <path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
  </symbol>

  <symbol id="icon-dropdown-arrow">
    <title>A down arrow</title>
    <path d="M19 9l-7 7-7-7"/>
  </symbol>

</svg>

O navegador lê o HTML SVG, coloca as informações do ícone na memória e continua com o restante da página referenciando o ID para outros usos do ícone, desta forma:

<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
  <use href="#icon-home" />
</svg>

<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
  <use href="#icon-dropdown-arrow" />
</svg>

DevTools mostrando um elemento de uso SVG renderizado.

Defina uma vez e use quantas vezes quiser, com impacto mínimo no desempenho da página e estilo flexível. Observe que aria-hidden="true" foi adicionado ao elemento SVG. Os ícones não são úteis para alguém que apenas ouve o conteúdo durante a navegação. Ocultá-los para esses usuários impede que eles adicionem ruído desnecessário.

É aqui que a navegação estrutural tradicional e as que estão neste componente divergem. Normalmente, seria apenas um link <a>, mas adicionei a UX de travessia com um select disfarçado. A classe .crumb é responsável por dispor o link e o ícone, enquanto o .crumbicon é responsável por empilhar o ícone e selecionar o elemento juntos. Já o chamei de link de divisão porque suas funções são muito parecidas com um botão de divisão, mas para navegação nas páginas.

<span class="crumb">
  <a href="#sub-collection-b">Category B</a>
  <span class="crumbicon">
    <svg>...</svg>
    <select class="disguised-select" title="Navigate to another category">
      <option>Category A</option>
      <option selected>Category B</option>
      <option>Category C</option>
    </select>
  </span>
</span>

Um link e algumas opções não são nada de especial, mas adicionam mais funcionalidade a uma navegação estrutural simples. Adicionar um title ao elemento <select> é útil para usuários de leitores de tela, fornecendo informações sobre a ação do botão. No entanto, ela vai oferecer a mesma ajuda a todas as outras pessoas, mas vai aparecer em destaque no iPad. Um atributo fornece contexto de botão para muitos usuários.

Captura de tela com o elemento de seleção invisível passando o cursor sobre ele e a
dica contextual exibida.

Decorações do separador

<span class="crumb-separator" aria-hidden="true">→</span>

Os separadores são opcionais, adicionar apenas um também funciona muito bem. Confira o terceiro exemplo no vídeo acima. Em seguida, dou a cada aria-hidden="true", já que eles são decorativos e não é algo que um leitor de tela precisa anunciar.

A propriedade gap, abordada a seguir, torna o espaçamento simples.

Estilos

Como a cor usa cores do sistema, são principalmente lacunas e pilhas para estilos.

Direção e fluxo do layout

DevTools mostrando o alinhamento de navegação da navegação estrutural com um recurso de sobreposição
flexbox.

O elemento de navegação principal nav.breadcrumbs define uma propriedade personalizada com escopo para os filhos usarem. Caso contrário, estabelece um layout horizontal alinhado verticalmente. Isso garante que as pistas, os divisores e os ícones se alinhem.

.breadcrumbs {
  --nav-gap: 2ch;

  display: flex;
  align-items: center;
  gap: var(--nav-gap);
  padding: calc(var(--nav-gap) / 2);
}

Uma navegação estrutural mostrada verticalmente com sobreposições de flexbox.

Cada .crumb também estabelece um layout horizontal na vertical com alguma lacuna, mas direciona especialmente os filhos do link e especifica o estilo white-space: nowrap. Isso é crucial para a navegação estrutural com várias palavras, porque não queremos que elas fiquem com várias linhas. Mais adiante nesta postagem, vamos adicionar estilos para lidar com o estouro horizontal que essa propriedade white-space causou.

.crumb {
  display: inline-flex;
  align-items: center;
  gap: calc(var(--nav-gap) / 4);

  & > a {
    white-space: nowrap;

    &[aria-current="page"] {
      font-weight: bold;
    }
  }
}

aria-current="page" foi adicionado para ajudar a destacar o link da página atual dos demais. Os usuários de leitores de tela terão uma indicação clara de que o link é para a página atual, mas também estilizamos o elemento visualmente para ajudar os usuários com deficiência visual a ter uma experiência do usuário semelhante.

O componente .crumbicon usa a grade para empilhar um ícone SVG com um elemento <select> "quase invisível".

Grid DevTools mostrada sobrepondo um botão em que a linha e a coluna são
chamadas de pilha.

.crumbicon {
  --crumbicon-size: 3ch;

  display: grid;
  grid: [stack] var(--crumbicon-size) / [stack] var(--crumbicon-size);
  place-items: center;

  & > * {
    grid-area: stack;
  }
}

O elemento <select> é o último no DOM e fica no topo da pilha e é interativo. Adicione um estilo de opacity: .01 para que o elemento ainda possa ser usado. O resultado será uma caixa de seleção que se encaixa perfeitamente na forma do ícone. Essa é uma boa maneira de personalizar a aparência de um elemento <select> e, ao mesmo tempo, manter a funcionalidade integrada.

.disguised-select {
  inline-size: 100%;
  block-size: 100%;
  opacity: .01;
  font-size: min(100%, 16px); /* Defaults to 16px; fixes iOS zoom */
}

Menu flutuante

Navegação estrutural deve ser capaz de representar um caminho muito longo. Gosto de permitir que coisas fiquem fora da tela horizontalmente, quando apropriado, e achei que esse componente de navegação estrutural se qualificou bem.

.breadcrumbs {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scroll-padding-inline: calc(var(--nav-gap) / 2);

  & > .crumb:last-of-type {
    scroll-snap-align: end;
  }

  @supports (-webkit-hyphens:none) { & {
    scroll-snap-type: none;
  }}
}

Os estilos de sobrecarga configuram a UX a seguir:

  • Rolagem horizontal com contenção de rolagem.
  • Padding de rolagem horizontal.
  • Um ponto de ajuste na última trilha. Isso significa que, no carregamento da página, as primeiras migrações de navegação são ajustadas e visíveis.
  • Remove o ponto de ajuste do Safari, que sofre com as combinações de rolagem horizontal e ajuste de efeitos.

Consultas de mídia

Um ajuste sutil para janelas de visualização menores é ocultar o rótulo "Página inicial", deixando apenas o ícone:

@media (width <= 480px) {
  .breadcrumbs .home-label {
    display: none;
  }
}

Ao lado da navegação estrutural com e sem um rótulo de início, para
comparação.

Acessibilidade

Movimento

Não há muito movimento nesse componente, mas, ao unir a transição em uma verificação de prefers-reduced-motion, podemos evitar movimentos indesejados.

@media (prefers-reduced-motion: no-preference) {
  .crumbicon {
    transition: box-shadow .2s ease;
  }
}

Nenhum dos outros estilos precisa mudar. Os efeitos de passar o cursor e focar são ótimos e significativos sem um transition, mas, se o movimento estiver correto, adicionaremos uma transição sutil à interação.

JavaScript

Primeiro, independentemente do tipo de roteador usado no site ou aplicativo, quando um usuário altera a navegação estrutural, o URL precisa ser atualizado e a página adequada é mostrada ao usuário. Em segundo lugar, para normalizar a experiência do usuário, verifique se navegações inesperadas não acontecem quando os usuários estiverem apenas procurando opções de <select>.

Duas medidas essenciais de experiência do usuário que serão processadas pelo JavaScript: o select mudou e a prevenção antecipada de disparo de eventos de mudança <select>.

A prevenção rápida de eventos é necessária devido ao uso de um elemento <select>. No Windows Edge e provavelmente em outros navegadores também, o evento changed selecionado é disparado à medida que o usuário navega pelas opções com o teclado. É por isso que eu chamo de "antecipada", já que o usuário apenas pseudoselecionou a opção, como passar o cursor ou focar, mas ainda não confirmou a escolha com enter ou click. O evento antecipado torna esse recurso de mudança de categoria de componente inacessível porque abrir a caixa de seleção e navegar em um item dispara o evento e altera a página antes que o usuário esteja pronto.

Um evento <select> melhor mudou

const crumbs = document.querySelectorAll('.breadcrumbs select')
const allowedKeys = new Set(['Tab', 'Enter', ' '])
const preventedKeys = new Set(['ArrowUp', 'ArrowDown'])

// watch crumbs for changes,
// ensures it's a full value change, not a user exploring options via keyboard
crumbs.forEach(nav => {
  let ignoreChange = false

  nav.addEventListener('change', e => {
    if (ignoreChange) return
    // it's actually changed!
  })

  nav.addEventListener('keydown', ({ key }) => {
    if (preventedKeys.has(key))
      ignoreChange = true
    else if (allowedKeys.has(key))
      ignoreChange = false
  })
})

A estratégia para isso é observar eventos de pressionamento de teclado em cada elemento <select> e determinar se a tecla pressionada foi uma confirmação de navegação (Tab ou Enter) ou navegação espacial (ArrowUp ou ArrowDown). Com essa determinação, o componente pode decidir aguardar ou avançar quando o evento do elemento <select> for disparado.

Conclusão

Agora que você sabe como eu fiz isso, o que você faria‽ 🙂

Vamos diversificar nossas abordagens e aprender todas as maneiras de criar na Web. Crie uma demonstração, envie um tweet para mim (link em inglês) e eu vou adicionar o conteúdo à seção de remixes da comunidade abaixo.

Remixes da comunidade