Como criar um componente de navegação estrutural

Uma visão geral básica de como criar um componente de navegação estruturada responsivo e acessível para os usuários navegarem no seu site.

Neste post, quero compartilhar uma maneira de criar componentes de trilha de navegação. Teste a demonstração.

Demo

Se preferir vídeos, confira a versão desta postagem no YouTube:

Visão geral

Um componente de localização atual mostra em que parte da hierarquia do site o usuário está. O nome vem de Hansel e Gretel, que deixaram pedaços de pão para trás em uma floresta escura e conseguiram encontrar o caminho de casa rastreando os pedaços de pão.

Os breadcrumbs nesta postagem não são padrões, são semelhantes a breadcrumbs. Eles oferecem mais funcionalidade colocando páginas irmãs diretamente na navegação com um <select>, tornando possível o acesso em vários níveis.

UX de 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 ao navegar pelo seguinte caminho: home » rpg » indie » on sale, conforme mostrado abaixo.

Esse componente de breadcrumbs 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 escolher. Na página inicial do protótipo de breadcrumb desta postagem, as coleções são FPS, RPG, brawler, dungeon crawler, esportes e quebra-cabeça.

Itens

Um videogame é um item, e 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 está na página da coleção. Por exemplo, eles estão na página de RPG, que mostra uma lista de jogos de RPG, incluindo as subcategorias AAA, Indie e "Self Published".

Em termos de ciência da computação, esse componente de breadcrumbs 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 terá uma arquitetura de informação (IA) personalizada que cria uma matriz multidimensional diferente, mas espero que o conceito de páginas de destino de coleção e a travessia de hierarquia também possam ser incluídos nos seus breadcrumbs.

Layouts

Marcação

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

Esquema escuro e claro

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

A metatag color-scheme no snippet acima informa ao navegador que esta página quer os estilos claro e escuro do navegador. Os exemplos de trilhas de navegação não incluem CSS para esses esquemas de cores. Por isso, eles usam as cores padrão fornecidas pelo navegador.

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

É apropriado usar o elemento <nav> para a navegação do site, que tem um papel de navegação ARIA implícito. Durante os testes, notei que o atributo role mudou a maneira como um Leitor de tela interagiu com o elemento. Ele foi anunciado como navegação, e por isso decidi 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 em todas as instâncias do ícone. Isso evita que as mesmas informações de caminho sejam repetidas, causando documentos maiores e o potencial de inconsistência do 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, como este:

<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 um impacto mínimo na performance da página e um estilo flexível. Observe que aria-hidden="true" foi adicionado ao elemento SVG. Os ícones não são úteis para alguém que está navegando e só ouve o conteúdo. Ocultar os ícones desses usuários evita que eles adicionem ruído desnecessário.

É aqui que o caminho de navegação tradicional e os caminhos neste componente divergem. Normalmente, esse seria apenas um link <a>, mas adicionei a UX de travessia com um seletor disfarçado. A classe .crumb é responsável por posicionar o link e o ícone, enquanto o .crumbicon é responsável por empilhar o ícone e o elemento selecionado. Chamei de link dividido porque as funções dele são muito semelhantes a um botão dividido, mas para navegação de página.

<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 especiais, mas adicionam mais funcionalidade a um breadcrumb 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, ele oferece a mesma ajuda para todos, e você vai encontrar essa opção na parte frontal do iPad. Um atributo fornece contexto de botão para muitos usuários.

Captura de tela com o elemento de seleção invisível sobre o qual o cursor está posicionado e a dica
contextual mostrada.

Decorações de separadores

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

Os separadores são opcionais, mas adicionar apenas um também funciona (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, simplifica o espaçamento entre esses elementos.

Estilos

Como a cor usa cores do sistema, ela tem principalmente lacunas e pilhas para estilos.

Direção e fluxo do layout

As ferramentas do desenvolvedor mostram o alinhamento de navegação estrutural com o recurso de sobreposição de flexbox.

O elemento de navegação principal nav.breadcrumbs define uma propriedade personalizada com escopo para que os filhos usem e, caso contrário, estabelece um layout horizontal alinhado verticalmente. Isso garante que as trilhas, os divisores e os ícones fiquem alinhados.

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

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

Um breadcrumb mostrado alinhado verticalmente com sobreposições de flexbox.

Cada .crumb também estabelece um layout alinhado verticalmente com alguma diferença, mas segmenta especialmente os links filhos e especifica o estilo white-space: nowrap. Isso é crucial para breadcrumbs com várias palavras, porque não queremos que eles tenham várias linhas. Mais adiante nesta postagem, vamos adicionar estilos para processar o overflow horizontal causado por essa propriedade white-space.

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

  & > a {
    white-space: nowrap;

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

O aria-current="page" é adicionado para ajudar o link da página atual a se destacar dos demais. Os usuários de leitores de tela não terão apenas um indicador claro de que o link é para a página atual, mas também estilizamos visualmente o elemento para ajudar os usuários com visão a ter uma experiência semelhante.

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

As ferramentas de desenvolvimento da grade são mostradas sobrepostas a 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, então ele está na parte de cima da pilha e é interativo. Adicione um estilo de opacity: .01 para que o elemento ainda seja utilizável, e 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>, mantendo 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

Os breadcrumbs precisam representar um caminho muito longo. Sou fã de permitir que as coisas saiam da tela horizontalmente, quando apropriado, e senti que esse componente de breadcrumbs 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 overflow configuram a seguinte UX:

  • Rolagem horizontal com contenção de rolagem.
  • Padding de rolagem horizontal.
  • Um ponto de ajuste na última marca. Isso significa que, no carregamento da página, o primeiro breadcrumb é carregado fixado e visível.
  • Remove o ponto de ajuste do Safari, que tem problemas com as combinações de rolagem horizontal e efeito de ajuste.

Consultas de mídia

Um ajuste sutil para visualizações menores é ocultar o rótulo "Início", deixando apenas o ícone:

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

Comparação lado a lado das linhas de caminho com e sem um marcador de página inicial.

Acessibilidade

Movimento

Não há muito movimento nesse componente, mas, ao agrupar a transição em uma verificação 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 foco e de passar o cursor são ótimos e significativos sem um transition, mas, se o movimento estiver correto, vamos adicionar uma transição sutil à interação.

JavaScript

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

Duas medidas importantes de experiência do usuário que precisam ser tratadas pelo JavaScript: o elemento de seleção mudou e a prevenção de disparo de evento de mudança <select> ansioso.

A prevenção de eventos apressados é necessária devido ao uso de um elemento <select>. No Windows Edge e provavelmente em outros navegadores, o evento de seleção changed é acionado quando o usuário navega pelas opções com o teclado. Por isso, chamamos de ansioso, já que o usuário apenas selecionou a opção de forma pseudo, como um passar o cursor ou o foco, mas não confirmou a escolha com enter ou click. O evento apressado torna esse recurso de mudança de categoria de componente inacessível, porque abrir a caixa de seleção e simplesmente navegar por um item aciona o evento e muda a página antes que o usuário esteja pronto.

Um evento <select> alterado melhor

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 os eventos de teclado pressionado em cada elemento <select> e determinar se a tecla pressionada foi a confirmação de navegação (Tab ou Enter) ou a navegação espacial (ArrowUp ou ArrowDown). Com essa determinação, o componente pode decidir esperar ou ir, quando o evento do elemento <select> é acionado.

Conclusão

Agora que você sabe como eu fiz, como você faria? 🙂

Vamos diversificar nossas abordagens e aprender todas as maneiras de criar na Web. Crie uma demonstração, envie links para mim e vou adicionar à seção de remixes da comunidade abaixo.

Remixes da comunidade