Uma visão geral básica de como criar um componente de guias semelhante aos encontrados em apps para iOS e Android.
Neste post, quero compartilhar ideias sobre como criar um componente de guias para a Web que seja responsivo, ofereça suporte a várias entradas de dispositivos e funcione em todos os navegadores. Confira a demonstração.
Se você preferir o vídeo, aqui está uma versão do YouTube desta postagem:
Visão geral
As guias são um componente comum dos sistemas de design, mas podem ter várias formas e
formatos. Primeiro, havia guias para computador criadas no elemento <frame>
. Agora, temos
componentes para dispositivos móveis que animam o conteúdo com base nas propriedades de física.
Todos eles tentam fazer a mesma coisa: economizar espaço.
Hoje, o essencial da experiência do usuário com as guias é uma área de navegação de botões que alterna a visibilidade do conteúdo em um frame de exibição. Muitas áreas de conteúdo diferentes compartilham o mesmo espaço, mas são apresentadas condicionalmente com base no botão selecionado na navegação.
Web Tactics
Resumindo, achei esse componente bem simples de criar, graças a 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 usando hashes de URL para suporte ao compartilhamento e ancoragem de rolagem na página processada pelo navegador
- Suporte a leitores de tela com marcação de elementos
<a>
eid="#hash"
prefers-reduced-motion
para ativar transições de transição cruzada e rolagem instantânea na página- O recurso da Web
@scroll-timeline
em rascunho para sublinhar dinamicamente e mudar a cor da guia selecionada
O HTML
Basicamente, a UX aqui é: clique em um link, faça com que o URL represente o estado da página aninhada e veja a área de conteúdo ser atualizada conforme o navegador rola até o elemento correspondente.
Há alguns membros de conteúdo estrutural: links e :target
s. Precisamos
de uma lista de links, para isso um <nav>
é ótimo, e uma lista de elementos <article>
, para isso um <section>
é ótimo. Cada hash de link corresponde a uma seção,
permitindo que o navegador role as coisas por meio de ancoragem.
Por exemplo, clicar em um link foca automaticamente o artigo :target
no
Chrome 89, sem necessidade de JS. O usuário pode rolar o conteúdo do artigo com
o dispositivo de entrada normalmente. É conteúdo complementar, conforme indicado no
markup.
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>
Posso estabelecer conexões entre os elementos <a>
e <article>
com as 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, preenchi os artigos com quantidades variadas de lorem e os links com títulos de diferentes comprimentos e conjuntos de imagens. Com o conteúdo, podemos começar a criar o layout.
Layouts de rolagem
Há três tipos diferentes de áreas de rolagem neste componente:
- A navegação (cor-de-rosa) pode ser rolada horizontalmente.
- A área de conteúdo (azul) é rolável horizontalmente.
- Cada item do artigo (verde) pode ser rolado verticalmente.
Há dois tipos diferentes de elementos envolvidos na rolagem:
- Uma janela
Uma caixa com dimensões definidas que tem o estilo de propriedadeoverflow
. - Uma superfície grande
Neste layout, são os contêineres de lista: links de navegação, seções de artigos e conteúdo de artigos.
Layout do <snap-tabs>
O layout de nível superior que escolhi foi o flex (Flexbox). Defini a direção como
column
, para que o cabeçalho e a seção sejam ordenados verticalmente. Essa é nossa primeira
janela de rolagem, e ela oculta tudo com overflow hidden. O cabeçalho e
a seção vão usar o overscroll em breve, como zonas individuais.
<snap-tabs> <header></header> <section></section> </snap-tabs>
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 againstneeding 100% */ flex-shrink: 0; /* fixes cross browser quarks */ min-block-size: fit-content; } }
Voltando ao diagrama colorido de três rolagens:
<header>
está preparado para ser o contêiner de rolagem (rosa).<section>
está preparado para ser o contêiner de rolagem (azul).
Os frames que destaquei abaixo com o VisBug nos ajudam a ver as janelas que os contêineres de rolagem criaram.
Layout de <header>
das guias
O próximo layout é quase o mesmo: uso o flex para criar a ordenação vertical.
<snap-tabs> <header> <nav></nav> <span class="snap-indicator"></span> </header> <section></section> </snap-tabs>
header { display: flex; flex-direction: column; }
O .snap-indicator
precisa se mover horizontalmente com o grupo de links, e
esse layout de cabeçalho ajuda a definir esse estágio. Não há elementos de posição absoluta aqui.
Em seguida, os estilos de rolagem. Podemos compartilhar os estilos de rolagem
entre as duas áreas de rolagem horizontal (cabeçalho e seção). Por isso, criei uma classe
de utilitário, .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, barras de rolagem ocultas para dispositivos de toque e, por último, ajuste de rolagem para bloquear áreas de apresentação de conteúdo. A ordem das guias do teclado é acessível, e todas as interações guiam o foco de forma natural. Os contêineres de ajuste de rolagem também recebem uma boa interação no estilo carrossel do teclado.
Layout do cabeçalho das guias <nav>
Os links de navegação precisam ser dispostos em uma linha, sem quebras de linha, centralizados verticalmente, e cada item de link precisa ser fixado no contêiner de fixação de rolagem. Trabalho rápido para o CSS 2021!
<nav> <a></a> <a></a> <a></a> <a></a> </nav>
nav { display: flex; & a { scroll-snap-align: start; display: inline-flex; align-items: center; white-space: nowrap; } }
Cada link tem estilos e tamanhos próprios. Portanto, o layout de navegação só precisa especificar a direção e o fluxo. As larguras exclusivas nos itens de navegação tornam a transição entre as guias divertida, já que o indicador ajusta a largura ao novo destino. Dependendo de quantos elementos estão aqui, o navegador renderizará uma barra de rolagem ou não.
Layout de <section>
das guias
Essa seção é um item flexível e precisa ser o consumidor dominante de espaço. Ele
também precisa criar colunas para os artigos serem colocados. Mais uma vez,
trabalho rápido para o CSS 2021! O block-size: 100%
estica esse elemento para preencher o
elemento pai o máximo possível. Em seguida, para o próprio layout, ele cria uma série de
colunas que são 100%
a largura do elemento pai. As porcentagens funcionam muito bem aqui,
porque escrevemos fortes restrições no pai.
<section> <article></article> <article></article> <article></article> <article></article> </section>
section { block-size: 100%; display: grid; grid-auto-flow: column; grid-auto-columns: 100%; }
É como se dissemos "expandir verticalmente o máximo possível, de maneira insistente"
(lembre o cabeçalho que definimos como flex-shrink: 0
: ele é uma defesa contra esse
push de expansão), que define a altura da linha para um conjunto de colunas de altura total. O
estilo auto-flow
instrui a grade a sempre posicionar os filhos em uma linha
horizontal, sem quebra de linha, exatamente o que queremos: transbordar a janela mãe.
Às vezes, acho difícil entender essas coisas. Esse elemento de seção se encaixa em uma caixa, mas também cria um conjunto de caixas. Espero que os recursos visuais e as explicações estejam ajudando.
Layout de <article>
das guias
O usuário precisa poder rolar o conteúdo do artigo, e as barras de rolagem só precisam aparecer se houver overflow. Esses elementos do artigo estão em uma posição organizada. Eles são simultaneamente um pai de rolagem e um filho de rolagem. O navegador está processando algumas interações complicadas de toque, mouse e teclado para nós.
<article> <h2></h2> <p></p> <p></p> <h2></h2> <p></p> <p></p> ... </article>
article { scroll-snap-align: start; overflow-y: auto; overscroll-behavior-y: contain; }
Eu escolhi que os artigos fossem fixados no scroller pai. Eu realmente gosto de como os itens de link de navegação e os elementos do artigo se encaixam no início inline dos respectivos contêineres de rolagem. Parece e é um relacionamento harmonioso.
O artigo é um filho da grade, e o tamanho dele é predeterminado para ser a área de visualização que queremos fornecer a UX de rolagem. Isso significa que não preciso de nenhum estilo de altura ou largura aqui. Só preciso definir como ele transborda. Defino overflow-y como "auto" e, em seguida, capturo as interações de rolagem com a propriedade overscroll-behavior.
Resumo de três áreas de rolagem
Abaixo, escolhi nas configurações do sistema a opção "Sempre mostrar as barras de rolagem". Acho que é duplamente importante que o layout funcione com essa configuração ativada, já que é para eu revisar o layout e a orquestração de rolagem.
Acho que mostrar a régua da barra de rolagem nesse componente ajuda a mostrar claramente onde estão as áreas de rolagem, a direção que elas suportam e como elas interagem entre si. Considere como cada um desses frames de janela de rolagem também são flexíveis ou pais de grade para um layout.
O DevTools pode ajudar a visualizar isso:
Os layouts de rolagem estão completos: ajuste, vinculação direta e acessibilidade por teclado. Base sólida para melhorias de UX, estilo e satisfação.
Destaque do recurso
Os elementos filhos inseridos na rolagem mantêm a posição fixa durante o redimensionamento. Isso significa que o JavaScript não precisa mostrar nada na rotação do dispositivo ou no redimensionamento do navegador. Teste no Modo dispositivo do Chromium DevTools selecionando qualquer modo, exceto Responsivo, e redimensionando o frame do dispositivo. Observe que o elemento permanece visível e bloqueado com o conteúdo. Essa opção está disponível desde que o Chromium atualizou a implementação para corresponder à especificação. Confira uma postagem do blog sobre o assunto.
Animação
O objetivo do trabalho de animação aqui é vincular claramente as interações com o feedback da interface. Isso ajuda a orientar ou auxiliar o usuário na descoberta (espera-se que) perfeita de todo o conteúdo. Vou adicionar movimento com propósito e de forma condicional. Agora os usuários podem especificar as preferências de movimento no sistema operacional, e eu adoro responder às preferências deles nas minhas interfaces.
Vou vincular um sublinhado de guia à posição de rolagem do artigo. O ajuste não é
apenas um alinhamento perfeito, ele também ancora o início e o fim de uma animação.
Isso mantém a <nav>
, que funciona como um
minimapa, conectada ao conteúdo.
Vamos verificar a preferência de movimento do usuário no CSS e no JS. Há
alguns lugares ótimos para isso.
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?
@media (prefers-reduced-motion: no-preference) {
.scroll-snap-x {
scroll-behavior: smooth;
}
}
Como estamos introduzindo o movimento aqui, e um movimento que o usuário não controla (como a rolagem), só aplicamos esse estilo se o usuário não tiver preferência no sistema operacional em relação ao movimento reduzido. Dessa forma, só introduzimos o movimento de rolagem para pessoas que concordam com isso.
Indicador de guias
O objetivo dessa animação é ajudar a associar o indicador ao estado
do conteúdo. Decidi colorir estilos de border-bottom
de transição suave para usuários
que preferem movimentos reduzidos e uma animação de transição suave de cor + rolagem vinculada
para usuários que não se importam com o movimento.
No Chromium Devtools, posso alternar a preferência e demonstrar os dois estilos de transição diferentes. Foi muito divertido criar 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;
}
}
Oculto a .snap-indicator
quando o usuário prefere movimento reduzido, já que
não preciso mais dele. Em seguida, substituo por estilos border-block-end
e um
transition
. Observe também na interação com as guias que o item de navegação ativo não
só tem um destaque sublinhado da marca, mas a cor do texto também é mais escura. O
elemento ativo tem maior contraste de cor de texto e um destaque brilhante com iluminação suave.
Apenas algumas linhas extras de CSS vão fazer com que alguém se sinta notado, no sentido de que estamos respeitando as preferências de movimento. Adoro isso.
@scroll-timeline
Na seção acima, mostrei como processar os estilos de transição suave de movimento reduzido. Nesta seção, vou mostrar como vinculei o indicador e uma área de rolagem. Agora vamos falar sobre alguns recursos experimentais divertidos. Espero que você esteja tão animado quanto eu.
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
);
Primeiro, verifico a preferência de movimento do usuário no JavaScript. Se o resultado
for false
, o que significa que o usuário prefere um movimento reduzido, nenhum
dos efeitos de movimento de vinculação de rolagem será executado.
if (motionOK) {
// motion based animation code
}
No momento da redação deste artigo, o suporte do navegador para
@scroll-timeline
é inexistente. É uma
especificação de rascunho com apenas
implementações experimentais. Ela tem um polyfill, que usamos nesta demonstração.
ScrollTimeline
Embora o CSS e o JavaScript possam criar linhas do tempo de rolagem, optei pelo JavaScript para usar as medições de elementos em tempo real 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. Ao criar um
ScrollTimeline
, defino o driver do link de rolagem, o scrollSource
.
Normalmente, uma animação na Web é executada em um período 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, é importante
indicar que 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 falta
o ingrediente final, pontos de estado para animar, também conhecidos como
keyframes.
Frames-chave dinâmicos
Há uma maneira realmente poderosa de usar CSS declarativo puro para animar com
@scroll-timeline
, mas a animação que escolhi fazer era muito dinâmica. Não há
como fazer a transição entre a largura de auto
, nem como criar dinamicamente
um número de frames-chave com base no comprimento das crianças.
O JavaScript sabe como receber essas informações. Por isso, vamos iterar sobre os elementos filhos e extrair os valores calculados no momento da 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
, desfaça a estrutura da posição offsetLeft
e retorne uma string
que a use como um valor translateX
. Isso cria quatro frames-chave de transformação para a
animação. O mesmo é feito para a largura. Cada um é perguntado qual é a largura dinâmica
e, em seguida, é usado como um valor de keyframe.
Confira um exemplo de saída com base nas minhas fontes e preferências de 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)"]
Frames-chave de largura:
[...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 agora será animado em quatro keyframes, dependendo da posição de ajuste de rolagem do seletor de seção. Os pontos de ajuste criam uma delimitação clara entre nossos frames-chave e realmente aumentam a sensação de sincronização da animação.
O usuário conduz a animação com a interação, observando a largura e a posição do indicador mudar de uma seção para a próxima, rastreando perfeitamente com a rolagem.
Talvez você não tenha notado, mas estou muito orgulhoso da transição de cores quando o item de navegação destacado é selecionado.
O cinza mais claro não selecionado aparece ainda mais recuado quando o item destacado tem mais contraste. É comum fazer a transição de cores para texto, como ao passar o cursor e quando selecionado, mas é um nível mais avançado fazer a transição dessa cor ao rolar, sincronizada com o indicador de sublinhado.
Veja como fiz isso:
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 linha de tempo de rolagem do indicador sublinhado. Uso a mesma linha do tempo de antes: como sua função é emitir uma marcação ao rolar, podemos usá-la em qualquer tipo de animação. Como fiz antes, criei quatro keyframes no loop e retornei 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. Caso contrário, ele terá uma cor de texto padrão. O loop aninhado torna isso relativamente
simples, já que o loop externo é cada item de navegação e o loop interno é cada
keyframe pessoal do navitem. verifico se o elemento do loop externo é igual
ao do loop interno e o uso para saber quando está selecionado.
Foi muito divertido escrever isso. Demais.
Mais melhorias em JavaScript
Vale lembrar que o núcleo do que estou mostrando aqui funciona sem JavaScript. Dito isso, vamos ver como podemos melhorar quando o JS estiver disponível.
Links diretos
Links diretos são um termo mais relacionado a dispositivos móveis, mas acho que a intenção do link direto se
encontra com guias, em que é possível compartilhar um URL diretamente para o conteúdo de uma guia. O
navegador vai navegar na página até o ID que corresponde ao hash do URL. Descobri
que esse 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 nem sempre clicam ou usam um teclado. Às vezes, eles apenas rolam a página, como deveriam poder fazer. Quando o controle deslizante da seção parar de rolar, o ponto de parada precisa ser correspondente à barra de navegação na parte de cima.
Veja como eu espero pelo fim do 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 inicie uma nova. Quando as seções pararem de rolar, não limpe o tempo limite e dispare 100 ms após o descanso. 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);
};
Supondo que a rolagem esteja fixada, dividir a posição de rolagem atual pela largura da área de rolagem deve resultar em um número inteiro, e não em um decimal. Em seguida, tento extrair um navitem do nosso cache usando esse índice calculado. Se ele encontrar algo, 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 limpando qualquer guia ativa no momento e, em seguida,
fornecendo ao item de navegação recebido o atributo de estado ativo. A chamada para scrollIntoView()
tem uma interação divertida com o CSS que vale a pena notar.
.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,
aninhado, criamos uma consulta de mídia que aplica
a rolagem smooth
se o usuário for tolerante a movimentos. O JavaScript pode fazer chamadas
livremente para rolar elementos na visualização, e o CSS pode gerenciar a UX de forma declarativa.
Às vezes, eles fazem uma combinação maravilhosa.
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 na framework favorita? 🙂
Vamos diversificar nossas abordagens e aprender todas as maneiras de criar na Web. Crie um Glitch, envie um tweet para sua versão e eu a adiciono à seção Remixes da comunidade abaixo.
Remixes da comunidade
- @devnook, @rob_dodson e @DasSurma com Componentes da Web: artigo.
- @jhvanderschee com botões: Codepen.