Como criar um componente de guias

Uma visão geral básica de como criar um componente de guias semelhante aos encontrados em apps iOS e Android.

Nesta postagem, quero compartilhar ideias sobre como criar um componente de guias para a Web. que seja responsivo, compatível com várias entradas de dispositivos e funcione em vários navegadores. Confira a demonstração.

Demonstração

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

Visão geral

Guias são um componente comum de sistemas de design, mas podem assumir muitas formas e formas. Primeiro, havia guias para computador criadas no elemento <frame> e agora temos componentes móveis incríveis que animam o conteúdo com base em propriedades da física. Todos estão tentando fazer a mesma coisa: economizar espaço.

Hoje, o essencial de uma experiência do usuário de guias é uma área de navegação com botões que alterna a visibilidade do conteúdo em um frame de exibição. Muitas opções áreas de conteúdo compartilham o mesmo espaço, mas são apresentadas condicionalmente com base nas botão selecionado na navegação.

a colagem está bastante caótica devido à enorme diversidade de estilos que a Web aplicou ao conceito do componente
Uma colagem de estilos de web design de componentes de guia dos últimos 10 anos

Táticas da Web

No geral, achei esse componente bem simples de criar, graças a um alguns recursos essenciais da plataforma da Web:

  • scroll-snap-points para interações elegantes de deslizar e teclado com posições de parada de rolagem apropriadas
  • Links diretos via hashes de URL para suporte para compartilhamento e âncora de rolagem na página gerenciados pelo navegador
  • Suporte a leitores de tela com marcação de elementos <a> e id="#hash"
  • prefers-reduced-motion para ativar transições de crossfade e instantâneos rolagem na página
  • O recurso da Web @scroll-timeline em rascunho para sublinhar dinamicamente e cor alterando a guia selecionada

O HTML

Basicamente, a UX é: clicar em um link, fazer com que o URL represente o o estado da página e veja a área de conteúdo ser atualizada conforme o navegador rola até a elemento correspondente.

Há alguns membros de conteúdo estrutural: links e :targets. Qa precisa de uma lista de links, para os quais um <nav> é ideal, e uma lista de <article>. para os quais um <section> é ótimo. Cada hash de link corresponderá a uma seção, permitindo que o navegador role itens por meio da ancoragem.

Um botão de link é clicado, deslizando no conteúdo em foco

Por exemplo, clicar em um link foca automaticamente o artigo :target na Chrome 89, sem JS. O usuário pode então rolar o conteúdo do artigo com o dispositivo de entrada, como sempre. É conteúdo complementar, conforme indicado no marcação.

Usei a seguinte marcação para organizar as guias:

<snap-tabs>
  <header>
    <nav>
      <a></a>
      <a></a>
      <a></a>
      <a></a>
    </nav>
  </header>
  <section>
    <article></article>
    <article></article>
    <article></article>
    <article></article>
  </section>
</snap-tabs>

É possível estabelecer conexões entre os elementos <a> e <article> com Propriedades href e id como esta:

<snap-tabs>
  <header>
    <nav>
      <a href="#responsive"></a>
      <a href="#accessible"></a>
      <a href="#overscroll"></a>
      <a href="#more"></a>
    </nav>
  </header>
  <section>
    <article id="responsive"></article>
    <article id="accessible"></article>
    <article id="overscroll"></article>
    <article id="more"></article>
  </section>
</snap-tabs>

Em seguida, eu preenchi os artigos com uma mistura de lorem e os links com uma tamanho misto e conjunto de imagens de títulos. Com o conteúdo para trabalhar, podemos começar o mesmo layout organizacional.

Layouts de rolagem

Há três tipos diferentes de áreas de rolagem nesse componente:

  • A navegação (rosa) é horizontal rolável
  • A área de conteúdo (azul) está horizontalmente. rolável
  • Cada item de artigo (verde) está verticalmente rolável.
Três caixas coloridas com setas direcionais coloridas delineando as áreas de rolagem e mostrando a direção da rolagem.

Há dois tipos diferentes de elementos envolvidos na rolagem:

  1. Uma janela
    Uma caixa com dimensões definidas que tem o overflow .
  2. Uma superfície superdimensionada
    Neste layout, é a lista de contêineres: nav links, artigos de seções e conteúdos de artigos.

Layout do <snap-tabs>

O layout de nível superior que escolhi foi flexível (Flexbox). Eu defini a direção para column, de modo que o cabeçalho e a seção sejam ordenados verticalmente. Esta é a nossa primeira janela de rolagem e oculta tudo com o excesso oculto. O cabeçalho e usará a rolagem em breve, como zonas individuais.

HTML
<snap-tabs>
  <header></header>
  <section></section>
</snap-tabs>
CSS
  snap-tabs {
  display: flex;
  flex-direction: column;

  /* establish primary containing box */
  overflow: hidden;
  position: relative;

  & > section {
    /* be pushy about consuming all space */
    block-size: 100%;
  }

  & > header {
    /* defend against 
needing 100% */ flex-shrink: 0; /* fixes cross browser quarks */ min-block-size: fit-content; } }

Voltando ao diagrama colorido de três rolagens:

  • <header> agora está preparado para ser o (rosa) contêiner de rolagem.
  • O <section> está preparado para ser a rolagem (azul). contêiner do Docker.

Os frames destacados abaixo com O VisBug nos ajuda a ver as janelas que que contêineres de rolagem criaram.

os elementos de cabeçalho e seção têm sobreposições de hotpink, destacando o espaço que ocupam no componente

Layout das guias <header>

O próximo layout é quase o mesmo: uso o flexível para criar a ordenação vertical.

HTML
<snap-tabs>
  <header>
    <nav></nav>
    <span class="snap-indicator"></span>
  </header>
  <section></section>
</snap-tabs>
CSS
header {
  display: flex;
  flex-direction: column;
}

A .snap-indicator precisa viajar horizontalmente com o grupo de links. este layout de cabeçalho ajuda a definir o cenário. Não há elementos de posição absoluta aqui.

os elementos nav e span.indicator têm sobreposições de hotpink, destacando o espaço que ocupam no componente

Em seguida, os estilos de rolagem. Podemos compartilhar os estilos de rolagem entre nossas duas áreas de rolagem horizontais (cabeçalho e seção), então criei um utilitário a classe .scroll-snap-x.

.scroll-snap-x {
  /* browser decide if x is ok to scroll and show bars on, y hidden */
  overflow: auto hidden;
  /* prevent scroll chaining on x scroll */
  overscroll-behavior-x: contain;
  /* scrolling should snap children on x */
  scroll-snap-type: x mandatory;

  @media (hover: none) {
    scrollbar-width: none;

    &::-webkit-scrollbar {
      width: 0;
      height: 0;
    }
  }
}

Cada um precisa de transbordamento no eixo X, contenção de rolagem para capturar a rolagem, escondido barras de rolagem para dispositivos sensíveis ao toque e, por último, ajuste de rolagem para bloquear o conteúdo áreas de apresentação. A ordem de tabulação do teclado está acessível e qualquer guia de interações a se concentrar naturalmente. Os contêineres de ajuste de rolagem também ficam com um bom estilo de carrossel do usuário no teclado.

Layout do cabeçalho <nav> da guia

Os links de navegação precisam ser dispostos em uma linha, sem quebras de linha, verticalmente centralizado, e cada item do link deve se ajustar ao contêiner de ajuste de rolagem. Swift trabalhar para o CSS de 2021!

HTML
<nav>
  <a></a>
  <a></a>
  <a></a>
  <a></a>
</nav>
CSS
  nav {
  display: flex;

  & a {
    scroll-snap-align: start;

    display: inline-flex;
    align-items: center;
    white-space: nowrap;
  }
}

Cada link é dimensionado e dimensionado automaticamente, então o layout de navegação só precisa especificar direção e fluxo. Larguras únicas em itens de navegação fazem a transição entre guias divertido enquanto o indicador ajusta sua largura de acordo com o novo alvo. Dependendo de quantos elementos estiverem aqui, o navegador renderizará ou não uma barra de rolagem.

os elementos a da navegação têm sobreposições de hotpink, destacando o espaço que ocupam no componente e onde ficam excedentes

Layout das guias <section>

Esta seção é um item flexível e precisa ser o consumidor dominante de espaço. Ela também precisa criar colunas para os artigos serem colocados. Mais uma vez, rápido trabalhar no CSS 2021! O block-size: 100% estica esse elemento para preencher o o máximo possível, para o próprio layout, ele cria uma série de colunas que têm 100% a largura do pai. As porcentagens funcionam muito bem aqui porque escrevemos fortes restrições sobre o pai.

HTML
<section>
  <article></article>
  <article></article>
  <article></article>
  <article></article>
</section>
CSS
  section {
  block-size: 100%;

  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 100%;
}

É como se dissesse "expandir verticalmente o máximo possível, de maneira insistente" Lembre-se do cabeçalho definido como flex-shrink: 0: ele é uma defesa contra esse envio de expansão), que define a altura da linha de um conjunto de colunas de altura total. A O estilo auto-flow instrui a grade a sempre dispor os filhos na horizontal linha, sem quebras, exatamente o que queremos; para ultrapassar a janela pai.

os elementos do artigo têm sobreposições de hotpink, destacando o espaço que ocupam no componente e onde transbordam

Às vezes, eu acho isso difícil de entender! Este elemento de seção é encaixar em uma caixa, mas também criou um conjunto de caixas. Espero que os recursos visuais estão ajudando.

Layout das guias <article>

O usuário deve conseguir rolar o conteúdo do artigo, e as barras de rolagem precisam só aparecem se houver um estouro. Esses elementos de artigo estão em uma posição Eles são simultaneamente um pai de rolagem e um filho de rolagem. A navegador está realmente lidando com algumas interações complicadas de toque, mouse e teclado para a gente.

HTML
<article>
  <h2></h2>
  <p></p>
  <p></p>
  <h2></h2>
  <p></p>
  <p></p>
  ...
</article>
CSS
article {
  scroll-snap-align: start;

  overflow-y: auto;
  overscroll-behavior-y: contain;
}

Optei por encaixar os artigos dentro do botão de rolagem pai. Eu gosto muito como os itens do link de navegação e os elementos do artigo são alinhados ao início em linha dos respectivos contêineres de rolagem. Ele parece e parece um ambiente harmonioso relação.

o elemento artigo e seus elementos filhos têm sobreposições de hotpink, descrevendo o espaço que ocupam no componente e a direção que transbordam

O artigo é uma grade secundária com tamanho predeterminado que é a janela de visualização. área em que queremos fornecer UX de rolagem. Isso significa que não preciso de altura nem largura aqui, só preciso definir como ele transborda. defini "overflowy" como automático, além de capturar as interações de rolagem com o comportamento de rolagem .

Resumo das três áreas de rolagem

Abaixo, escolhi nas configurações do sistema a opção "Sempre mostrar as barras de rolagem". eu acho é duplamente importante que o layout funcione com essa configuração ativada, pois ela é analisar o layout e a orquestração de rolagem.

as três barras de rolagem estão configuradas para exibição, agora consumindo espaço de layout, e nosso componente ainda tem uma ótima aparência

Acho que ver a medianiz da barra de rolagem nesse componente ajuda a mostrar claramente onde quais são as áreas de rolagem, a direção delas e como elas interagem com uns aos outros. Considere como cada um desses frames da janela de rolagem também é flexionado ou pais de grade a um layout.

O DevTools pode nos ajudar a visualizar isso:

as áreas de rolagem têm sobreposições de ferramentas de grade e flexbox, destacando o espaço que ocupam no componente e a direção que transbordam
Chromium Devtools, mostrando o layout do elemento de navegação flexbox cheio de elementos âncora, o layout da seção em grade cheio de elementos do artigo, e o artigo elementos cheios de parágrafos e um elemento de título.

Os layouts de rolagem estão completos: ajuste, link direto e teclado acessíveis. Base sólida para melhorias, estilo e satisfação de UX.

Destaque do recurso

A rolagem dos filhos cortados mantém a posição fixa durante o redimensionamento. Isso significa que O JavaScript não precisa exibir nada na rotação do dispositivo ou no navegador. redimensionar. Teste no dispositivo Chromium DevTools Modo por selecionando qualquer modo diferente de Responsivo e, em seguida, redimensionando o frame do dispositivo. Observe que o elemento permanece visível e bloqueado com o conteúdo. Isso foi disponível desde que o Chromium atualizou sua implementação para corresponder à especificação. Aqui está uma postagem do blog sobre isso.

Animação

O objetivo do trabalho de animação aqui é vincular claramente as interações com a interface feedback. Isso ajuda a orientar ou ajudar o usuário no processo descoberta perfeita de todo o conteúdo. Vou adicionar movimento com propósito e condicionalmente. Os usuários podem especificar o movimento preferências do sistema operacional, e gosto muito de responder às preferências deles nas minhas interfaces.

Vou vincular um sublinhado de tabulação à posição de rolagem do artigo. O ajuste não é apenas o alinhamento, mas também está ancorando o início e o fim de uma animação. Isso mantém o <nav>, que age como uma mini-mapa, conectado ao conteúdo. Verificaremos a preferência de movimento do usuário no CSS e no JS. Há um alguns lugares ótimos para considerar!

Comportamento de rolagem

Há uma oportunidade de melhorar o comportamento de movimento de :target e element.scrollIntoView(). Por padrão, é instantâneo. O navegador apenas define a posição de rolagem. E se quisermos fazer a transição para essa posição de rolagem, em vez de piscar ali?

@media (prefers-reduced-motion: no-preference) {
  .scroll-snap-x {
    scroll-behavior: smooth;
  }
}

Já que estamos introduzindo movimento aqui, e que o usuário não controla (como a rolagem), só aplicamos esse estilo se o usuário não tiver preferência em o sistema operacional em torno da movimentação reduzida. Dessa forma, introduzimos apenas a rolagem e movimentos para as pessoas que concordam com isso.

Indicador de guias

O objetivo desta animação é ajudar a associar o indicador ao estado do conteúdo. Decidi aplicar o crossfade de cores border-bottom para os usuários que preferem movimento reduzido e uma animação deslizante + esmaecimento de cores vinculada à rolagem para usuários que concordam com o movimento.

No Chromium Devtools, posso alternar a preferência e demonstrar os dois diferentes estilos de transição. Eu me diverti muito criando isso.

@media (prefers-reduced-motion: reduce) {
  snap-tabs > header a {
    border-block-end: var(--indicator-size) solid hsl(var(--accent) / 0%);
    transition: color .7s ease, border-color .5s ease;

    &:is(:target,:active,[active]) {
      color: var(--text-active-color);
      border-block-end-color: hsl(var(--accent));
    }
  }

  snap-tabs .snap-indicator {
    visibility: hidden;
  }
}

Escondi a .snap-indicator quando o usuário prefere movimento reduzido porque não não precisam mais dela. Depois, substituo por estilos border-block-end e transition. Observe também na interação das guias que o item de navegação ativo não tem apenas o sublinhado da marca em destaque, mas a cor do texto também é mais escura. A o elemento ativo tem maior contraste de cor do texto e um destaque brilhante e com iluminação especial.

Bastam algumas linhas extras de CSS para que alguém se sinta visto (no sentido de que estamos respeitando cuidadosamente suas preferências de movimento). Adoro isso.

@scroll-timeline

Na seção acima, mostrei como lidar com o crossfade de movimento reduzido e, nesta seção, mostrarei como vinculei o indicador área de rolagem. Isso é algo divertido e experimental a seguir. Espero que você esteja tão animada quanto eu.

const { matches:motionOK } = window.matchMedia(
  '(prefers-reduced-motion: no-preference)'
);

Primeiro, confiro a preferência de movimento do usuário no JavaScript. Se o resultado do for false, o que significa que o usuário prefere movimento reduzido, então não executaremos nenhuma dos efeitos de movimento da vinculação de rolagem.

if (motionOK) {
  // motion based animation code
}

No momento em que este artigo foi escrito, o suporte do navegador para @scroll-timeline é nenhum. É um especificação de rascunho apenas com implementações experimentais. No entanto, ela tem um polyfill, que uso neste demonstração.

ScrollTimeline

Embora CSS e JavaScript possam criar linhas do tempo de rolagem, optei por JavaScript para que eu pudesse usar medidas de elementos ativos na animação.

const sectionScrollTimeline = new ScrollTimeline({
  scrollSource: tabsection,  // snap-tabs > section
  orientation: 'inline',     // scroll in the direction letters flow
  fill: 'both',              // bi-directional linking
});

Quero que uma coisa siga a posição de rolagem de outra e, criando uma ScrollTimeline Defino o driver do link de rolagem, o scrollSource. Normalmente, uma animação na Web é executada em um limite de tempo global, mas com um sectionScrollTimeline personalizado na memória, posso mudar tudo isso.

tabindicator.animate({
    transform: ...,
    width: ...,
  }, {
    duration: 1000,
    fill: 'both',
    timeline: sectionScrollTimeline,
  }
);

Antes de entrar nos frames-chave da animação, acho que é importante apontar o seguidor da rolagem, tabindicator, será animado com base em uma linha do tempo personalizada, a rolagem da seção. Isso conclui a vinculação, mas é sem o ingrediente final, pontos com estado entre os quais animar, também conhecidos como frames-chave.

Frames-chave dinâmicos

Existe uma maneira muito poderosa de CSS puro e declarativo de animar @scroll-timeline, mas a animação que escolhi era muito dinâmica. Não há forma de transição entre a largura de auto, e não há como criar dinamicamente uma série de frames-chave com base no comprimento dos filhos.

No entanto, o JavaScript sabe como obter essas informações, então vamos iterar nas nossos filhos e extrair os valores calculados no ambiente de execução:

tabindicator.animate({
    transform: [...tabnavitems].map(({offsetLeft}) =>
      `translateX(${offsetLeft}px)`),
    width: [...tabnavitems].map(({offsetWidth}) =>
      `${offsetWidth}px`)
  }, {
    duration: 1000,
    fill: 'both',
    timeline: sectionScrollTimeline,
  }
);

Para cada tabnavitem, desestruturar a posição offsetLeft e retornar uma string que o usa como um valor translateX. Isso cria quatro frames-chave de transformação para o animação. O mesmo se aplica à largura, sendo perguntados a cada qual a largura dinâmica e usado como um valor de frame-chave.

Este é um exemplo de saída, com base nas minhas preferências de fonte e navegador:

Frames-chave TranslateX:

[...tabnavitems].map(({offsetLeft}) =>
  `translateX(${offsetLeft}px)`)

// results in 4 array items, which represent 4 keyframe states
// ["translateX(0px)", "translateX(121px)", "translateX(238px)", "translateX(464px)"]

Larguras-chave de frames-chave:

[...tabnavitems].map(({offsetWidth}) =>
  `${offsetWidth}px`)

// results in 4 array items, which represent 4 keyframe states
// ["121px", "117px", "226px", "67px"]

Para resumir a estratégia, o indicador de guia será animado em quatro frames-chave de acordo com a posição do ajuste de rolagem do botão de rolagem da seção. Os pontos de ajuste criar uma delineação clara entre nossos frames-chave e realmente aumentar sensação sincronizada da animação.

as guias ativa e inativa são mostradas com sobreposições do VisBug, que mostram pontuações de contraste de passagem para ambas

O usuário conduz a animação com a interação, vendo a largura e a posição do indicador mudar de uma seção para outra, acompanhando perfeitamente com a rolagem.

Talvez você não tenha percebido, mas estou muito orgulhoso da transição das cores como o item de navegação destacado é selecionado.

O cinza-claro não selecionado aparece ainda mais atrasado quando o cinza-claro não selecionado item tem mais contraste. É comum mudar a cor do texto, como ao passar o cursor e quando selecionada, mas é o próximo nível fazer a transição dessa cor ao rolar, sincronizados com o indicador de sublinhado.

Eu fiz o seguinte:

tabnavitems.forEach(navitem => {
  navitem.animate({
      color: [...tabnavitems].map(item =>
        item === navitem
          ? `var(--text-active-color)`
          : `var(--text-color)`)
    }, {
      duration: 1000,
      fill: 'both',
      timeline: sectionScrollTimeline,
    }
  );
});

Cada link de navegação de guia precisa dessa nova animação de cor, rastreando a mesma rolagem linha do tempo como indicador de sublinhado. Uso o mesmo cronograma de antes, já que sua função é emitir uma marcação na rolagem, podemos usá-la em qualquer tipo animação que queremos. Como fiz antes, crio quatro frames-chave no loop e retorno cores.

[...tabnavitems].map(item =>
  item === navitem
    ? `var(--text-active-color)`
    : `var(--text-color)`)

// results in 4 array items, which represent 4 keyframe states
// [
  "var(--text-active-color)",
  "var(--text-color)",
  "var(--text-color)",
  "var(--text-color)",
]

O frame-chave com a cor var(--text-active-color) destaca o link. é uma cor de texto padrão. Esse loop aninhado o torna relativamente simples, pois o loop externo é cada item de navegação, e o loop interno é frames-chave pessoais do navitem. verifico se o elemento do loop externo é o mesmo o loop interno um e usá-lo para saber quando é selecionado.

Eu me diverti muito escrevendo isso. Demais.

Ainda mais melhorias no JavaScript

Vale lembrar que a essência do que estou mostrando aqui funciona sem JavaScript. Dito isso, vamos ver como é possível melhorá-lo quando o JS é disponíveis.

Links diretos são um termo mais relacionado a dispositivos móveis, mas acho que a intenção do link direto é aqui com guias, no qual você pode compartilhar um URL diretamente para o conteúdo de uma guia. A navegador navegará in-page até o ID correspondente no hash do URL. Encontrei este gerenciador onload fez o efeito em várias plataformas.

window.onload = () => {
  if (location.hash) {
    tabsection.scrollLeft = document
      .querySelector(location.hash)
      .offsetLeft;
  }
}

Rolar a tela para finalizar a sincronização

Nossos usuários não estão sempre clicando ou usando o teclado, às vezes eles estão apenas rolagem livre, como deveriam. Quando o botão de rolagem da seção para rolagem, onde quer que ela chegue precisa ser correspondida na barra de navegação superior.

Veja como espero até o fim da rolagem: js tabsection.addEventListener('scroll', () => { clearTimeout(tabsection.scrollEndTimer); tabsection.scrollEndTimer = setTimeout(determineActiveTabSection, 100); });

Sempre que as seções estiverem sendo roladas, limpe o tempo limite da seção, se houver, e iniciar um novo. Quando a rolagem das seções é interrompida, não apague o tempo limite, e acionar 100 ms após o repouso. Quando disparar, chame a função que busca descobrir onde o usuário parou.

const determineActiveTabSection = () => {
  const i = tabsection.scrollLeft / tabsection.clientWidth;
  const matchingNavItem = tabnavitems[i];

  matchingNavItem && setActiveTab(matchingNavItem);
};

Considerando que a rolagem foi ajustada, dividindo a posição de rolagem atual da largura da área de rolagem deve resultar em um número inteiro, e não um decimal. Então eu tento obter um item de navegação do cache por meio desse índice calculado e, se encontrar envio a correspondência para ser ativada.

const setActiveTab = tabbtn => {
  tabnav
    .querySelector(':scope a[active]')
    .removeAttribute('active');

  tabbtn.setAttribute('active', '');
  tabbtn.scrollIntoView();
};

A configuração da guia ativa começa apagando qualquer guia ativa no momento e concedendo ao item de navegação de entrada, ao atributo de estado ativo. Chamada para scrollIntoView() tenha uma interação divertida com o CSS que vale a pena ressaltar.

.scroll-snap-x {
  overflow: auto hidden;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;

  @media (prefers-reduced-motion: no-preference) {
    scroll-behavior: smooth;
  }
}

No CSS do utilitário de ajuste de rolagem horizontal, nested: uma consulta de mídia que se aplica Rolagem smooth se o usuário for tolerante a movimentos. O JavaScript pode tornar livremente chamadas para rolar os elementos para visualização, e o CSS pode gerenciar a UX de maneira declarativa. É o que eles fazem às vezes.

Conclusão

Agora que você sabe como eu fiz isso, como faria?! Com isso, vamos nos divertir da arquitetura de componentes. Quem vai criar a primeira versão com slots nos framework favorito? 🙂

Vamos diversificar nossas abordagens e aprender todas as maneiras de criar na Web. Crie um Glitch e me twittou sua versão e eu a adiciono aos remixes da comunidade seção abaixo.

Remixes da comunidade