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.
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.
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>
eid="#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 :target
s. 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.
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.
Há dois tipos diferentes de elementos envolvidos na rolagem:
- Uma janela
Uma caixa com dimensões definidas que tem ooverflow
. - 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.
<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>
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.
Layout das guias <header>
O próximo layout é quase o mesmo: uso o flexível 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; }
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.
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!
<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 é 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.
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.
<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 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.
À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.
<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; }
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 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.
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:
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.
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
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
- @devnook, @rob_dodson e @DasSurma com componentes da Web: artigo.
- @jhvanderschee com botões: Codepen.