Como criar um componente de navegação estrutural

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

Nesta postagem, quero compartilhar uma forma de criar componentes de rastreamento. Teste a demonstração.

Demonstração

Se preferir vídeo, confira uma versão deste post no YouTube:

Visão geral

Um componente de trilha de navegação mostra onde o usuário está na hierarquia do site. O nome vem de Hansel e Gretel, que deixaram migalhas de pão para trás em uma floresta escura e conseguiram encontrar o caminho de casa seguindo as migalhas.

Os rastros neste post não são rastros padrão, são semelhantes a rastros. Elas oferecem mais funcionalidades ao colocar páginas irmãs diretamente na navegação com um <select>, possibilitando o acesso em vários níveis.

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. Para criar esse rastreamento, navegue pelo seguinte caminho: home » rpg » indie » on sale, conforme mostrado abaixo.

Esse componente de rastreamento permite que os usuários naveguem pela hierarquia de informações, pulando ramificações e selecionando páginas com rapidez 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 navegação deste post, as coleções são FPS, RPG, luta, 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, 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 rastros de navegação representa um array 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ções (AI) personalizada, criando uma matriz multidimensional diferente, mas espero que o conceito de páginas de destino da coleção e de navegação na hierarquia também possa ser usado no seu rastreamento de navegação.

Layouts

Marcação

Bons componentes começam com HTML adequado. Na próxima seção, vou abordar minhas opções 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 esta página quer os estilos claro e escuro do navegador. Os exemplos de rastros não incluem CSS para esses esquemas de cores, então eles vão usar as cores padrão fornecidas pelo navegador.

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

É adequado usar o elemento <nav> para a navegação do site, que tem uma função ARIA implícita de navegação. Nos testes, percebi que ter o atributo role mudou a maneira como um leitor de tela interagia com o elemento. Ele foi anunciado como navegação, então 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 possível inconsistência de caminho.

Para usar essa técnica, adicione um elemento SVG oculto à página e encapsule 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, assim:

<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" é adicionado ao elemento SVG. Os ícones não são úteis para quem está navegando e só ouve o conteúdo. Ocultá-los desses usuários evita ruídos desnecessários.

É aqui que o breadcrumb tradicional e os deste componente divergem. Normalmente, esse seria apenas um link <a>, mas adicionei uma UX de navegação com uma seleção disfarçada. A classe .crumb é responsável por criar o link e o ícone, enquanto o .crumbicon é responsável por empilhar o ícone e selecionar o elemento juntos. Chamei de link dividido porque as funções dele são muito parecidas com um botão dividido, mas para navegação na 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 também oferece a mesma ajuda para todos. Ele fica na frente e no centro do iPad. Um atributo fornece contexto de botão para muitos usuários.

Captura de tela com o elemento de seleção invisível sendo passado com o cursor e a dica contextual aparecendo.

Decorações de separador

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

Os separadores são opcionais, e adicionar apenas um também funciona muito bem. Consulte 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, facilita o espaçamento.

Estilos

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

Direção e fluxo do layout

O DevTools mostrando o alinhamento da 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 uso dos filhos e, caso contrário, estabelece um layout horizontal alinhado verticalmente. Isso garante que os breadcrumbs, divisores e ícones estejam alinhados.

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

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

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

Cada .crumb também estabelece um layout horizontal alinhado verticalmente com alguma lacuna, mas segmenta especialmente os filhos de link e especifica o estilo white-space: nowrap. Isso é crucial para rastros de navegação com várias palavras, porque não queremos que eles sejam multilinha. Mais adiante nesta postagem, vamos adicionar estilos para lidar com o transbordamento 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 do restante. Além de os usuários de leitores de tela terem um indicador claro de que o link é para a página atual, 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".

DevTools da grade mostrando uma sobreposição em um botão em que a linha e a coluna são chamadas de &quot;stack&quot;.

.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á no topo da pilha e é interativo. Adicione um estilo de opacity: .01 para que o elemento ainda possa ser usado, e o resultado seja uma caixa de seleção que se encaixe 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. Gosto de permitir que as coisas saiam da tela horizontalmente, quando apropriado, e senti que esse componente de rastros de navegação se qualificava 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 estouro configuram a seguinte UX:

  • Rolagem horizontal com contenção de rolagem.
  • Padding de rolagem horizontal.
  • Um ponto de ajuste no último breadcrumb. Isso significa que, no carregamento da página, o primeiro elemento é carregado ajustado e em exibição.
  • Remove o ponto de ajuste do Safari, que tem dificuldades com as combinações de rolagem horizontal e efeito de ajuste.

Consultas de mídia

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

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

Comparação lado a lado dos rastros de navegação com e sem um rótulo de página inicial.

Acessibilidade

Movimento

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

JavaScript

Primeiro, independente do tipo de roteador usado no seu 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 de <select>.

Duas medidas importantes de experiência do usuário a serem processadas pelo JavaScript: select has changed e prevenção de disparo de evento de mudança <select> ansioso.

A prevenção de eventos antecipados é necessária devido ao uso de um elemento <select>. No Windows Edge e provavelmente em outros navegadores também, o evento select changed é acionado quando o usuário navega pelas opções com o teclado. Por isso, chamei de "ansioso", já que o usuário apenas pseudo selecionou a opção, como um passar o cursor ou um foco, mas ainda não confirmou a escolha com enter ou um click. O evento ansioso 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 vai disparar o evento e mudar a página antes que o usuário esteja pronto.

Um evento de mudança de <select> 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 é monitorar eventos de pressionamento de tecla em cada elemento <select> e determinar se a tecla pressionada foi de confirmação de navegação (Tab ou Enter) ou de navegação espacial (ArrowUp ou ArrowDown). Com essa determinação, o componente pode decidir esperar ou ir, quando o evento do elemento <select> for 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, me envie um tweet com o link, e eu vou adicionar à seção de remixes da comunidade abaixo.

Remixes da comunidade