Se você não estiver familiarizado com componentes de passo, seu objetivo principal é melhorar a experiência do usuário organizando grandes blocos de conteúdo lógico em etapas sequenciais menores. Um caso de uso generalizado desse componente é a criação de um checkout em várias etapas em sites de comércio eletrônico.
Nosso componente de guia
Aqui está o que vamos criar—redimensione seu navegador para ver como o layout da guia muda:
Não focaremos tanto na acessibilidade neste tutorial, então explorar como tornar esse componente mais acessível seria um próximo passo válido.
1. Comece com a marcação da página
Dentro de um container, colocaremos duas listas que incluem as abas e seus conteúdos associados (painéis).
Por padrão, a primeira aba estará ativa.
Aqui está a marcação necessária:
1 |
|
2 |
|
3 |
|
4 |
href="">
|
5 |
class="dot">
|
6 |
...
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
2. Adicione o CSS
Vamos nos concentrar nos estilos principais – você pode ver todos eles clicando no botão CSS guia do projeto de demonstração.
Em telas grandes (>700px), o componente da aba será assim:
Nos menores, ficará assim:
Observe como o stepper alterna entre a orientação horizontal e vertical dependendo do tamanho da tela.
Além disso, considere que todos os painéis de guias serão empilhados e movidos 100% para a esquerda; a qualquer momento, apenas aquele com o active
a turma aparecerá e ficará em sua posição inicial.
Aqui está uma parte dos estilos necessários:
1 |
/*CUSTOM VARIABLES HERE*/
|
2 |
|
3 |
.grid { |
4 |
display: grid; |
5 |
grid-template-columns: auto auto; |
6 |
gap: 70px; |
7 |
max-width: 1000px; |
8 |
padding: 0 20px; |
9 |
margin: 0 auto; |
10 |
}
|
11 |
|
12 |
.tab-list li { |
13 |
display: flex; |
14 |
}
|
15 |
|
16 |
.tab-list li:not(:last-child) { |
17 |
margin-bottom: 40px; |
18 |
}
|
19 |
|
20 |
.tab-list a { |
21 |
display: inline-flex; |
22 |
align-items: center; |
23 |
gap: 24px; |
24 |
text-decoration: none; |
25 |
}
|
26 |
|
27 |
.tab-list a .dot { |
28 |
position: relative; |
29 |
display: inline-block; |
30 |
width: 32px; |
31 |
height: 32px; |
32 |
border-radius: 50%; |
33 |
border: 1px solid var(--stepper-outline-color); |
34 |
}
|
35 |
|
36 |
.tab-list li a .dot::before, |
37 |
.tab-list li:not(:last-child) a .dot::after { |
38 |
content: ""; |
39 |
position: absolute; |
40 |
left: 50%; |
41 |
}
|
42 |
|
43 |
.tab-list li a .dot::before { |
44 |
top: 50%; |
45 |
transform: translate(-50%, -50%) scale(0); |
46 |
width: 18px; |
47 |
height: 18px; |
48 |
border-radius: 50%; |
49 |
background: var(--stepper-active-color); |
50 |
transition: transform 0.3s; |
51 |
}
|
52 |
|
53 |
.tab-list li:not(:last-child) a .dot::after { |
54 |
top: calc(100% + 1px); |
55 |
transform: translateX(-50%); |
56 |
height: 40px; |
57 |
border-left: 2px dashed var(--stepper-connector-color); |
58 |
}
|
59 |
|
60 |
.tab-list li.active a { |
61 |
font-weight: bold; |
62 |
}
|
63 |
|
64 |
.tab-list li.active a .dot::before { |
65 |
transform: translate(-50%, -50%) scale(1); |
66 |
}
|
67 |
|
68 |
.tab-panels { |
69 |
display: grid; |
70 |
overflow: hidden; |
71 |
}
|
72 |
|
73 |
.tab-panels > li { |
74 |
grid-area: 1/1; |
75 |
opacity: 0; |
76 |
transform: translateX(-100%); |
77 |
transition: opacity 0.35s ease-in-out, transform 0.7s ease-in-out; |
78 |
}
|
79 |
|
80 |
.tab-panels > li.active { |
81 |
opacity: 1; |
82 |
transform: none; |
83 |
}
|
84 |
|
85 |
@media (max-width: 700px) { |
86 |
.grid { |
87 |
grid-template-columns: 1fr; |
88 |
gap: 30px; |
89 |
}
|
90 |
|
91 |
.tab-list { |
92 |
display: flex; |
93 |
justify-content: center; |
94 |
}
|
95 |
|
96 |
.tab-list li:not(:last-child) { |
97 |
margin: 0 40px 0 0; |
98 |
}
|
99 |
|
100 |
.tab-list li a span:last-child { |
101 |
display: none; |
102 |
}
|
103 |
|
104 |
.tab-list a { |
105 |
gap: 0; |
106 |
}
|
107 |
|
108 |
.tab-list li:not(:last-child) a .dot::after { |
109 |
top: 50%; |
110 |
left: calc(100% + 1px); |
111 |
transform: translateY(-50%); |
112 |
width: 40px; |
113 |
height: auto; |
114 |
border-bottom: 2px dashed var(--stepper-connector-color); |
115 |
border-left: 0; |
116 |
}
|
117 |
}
|
3. Adicione o JavaScript
Cada vez que clicarmos em um link de guia, removeremos o active
classe da guia e painel atualmente ativos. Em seguida, colocaremos essa classe na guia e no painel associado a esse link.
Aqui está o JavaScript necessário:
1 |
const tabList = document.querySelector(".tab-list"); |
2 |
const tabItems = tabList.querySelectorAll("li"); |
3 |
const tabLinks = tabList.querySelectorAll("a"); |
4 |
const tabPanelsList = document.querySelector(".tab-panels"); |
5 |
const tabPanels = tabPanelsList.querySelectorAll("li"); |
6 |
const ACTIVE_CLASS = "active"; |
7 |
|
8 |
for (const tabLink of tabLinks) { |
9 |
tabLink.addEventListener("click", function (e) { |
10 |
e.preventDefault(); |
11 |
tabList.querySelector(`li.${ACTIVE_CLASS}`).classList.remove(ACTIVE_CLASS); |
12 |
tabPanelsList
|
13 |
.querySelector(`li.${ACTIVE_CLASS}`) |
14 |
.classList.remove(ACTIVE_CLASS); |
15 |
|
16 |
const parent = tabLink.parentElement; |
17 |
let parentIndex = Array.from(tabItems).indexOf(parent); |
18 |
parent.classList.add(ACTIVE_CLASS); |
19 |
tabPanelsList
|
20 |
.querySelector(`li:nth-child(${++parentIndex})`) |
21 |
.classList.add(ACTIVE_CLASS); |
22 |
});
|
23 |
}
|
Adicionar suporte para teclado
Embora nosso componente não esteja otimizado para acessibilidade, vamos adicionar suporte para navegação por teclado.
Em telas pequenas, cada vez que o esquerda (←) ou direita (→) teclas de seta são pressionados, pegaremos a guia atualmente ativa. A partir daí, verificaremos qual seta foi clicada. EUSe essa for a seta para a direita, definiremos a próxima guia ativa como aquela que segue imediatamente a guia ativa atual. Se não existir tal guia, a próxima guia se tornará a primeira. Da mesma forma, se a seta para a esquerda for clicada, definiremos a próxima guia como aquela que precede imediatamente a guia atualmente ativa. Se não existir tal guia, a próxima guia se tornará a última.
Seguiremos o mesmo processo com o para cima (↑) e para baixo (↓) teclas em telas grandes.
Aqui está o código JavaScript relevante:
1 |
...
|
2 |
|
3 |
tabList.addEventListener("keyup", function (e) { |
4 |
const activeTabListItem = tabList.querySelector(`li.${ACTIVE_CLASS}`); |
5 |
|
6 |
if ( |
7 |
e.key === "ArrowUp" || |
8 |
e.key === "ArrowDown" || |
9 |
e.key === "ArrowLeft" || |
10 |
e.key === "ArrowRight" |
11 |
) { |
12 |
if ( |
13 |
(mqSm.matches && (e.key === "ArrowUp" || e.key === "ArrowDown")) || |
14 |
(mqLg.matches && (e.key === "ArrowLeft" || e.key === "ArrowRight")) |
15 |
) { |
16 |
return; |
17 |
}
|
18 |
|
19 |
if (e.key === "ArrowUp" || e.key === "ArrowLeft") { |
20 |
const prevActiveTabListItem = activeTabListItem.previousElementSibling |
21 |
? activeTabListItem.previousElementSibling |
22 |
: lastTabListItem; |
23 |
prevActiveTabListItem.querySelector("a").click(); |
24 |
} else { |
25 |
const nextActiveTabListItem = activeTabListItem.nextElementSibling |
26 |
? activeTabListItem.nextElementSibling |
27 |
: firstTabListItem; |
28 |
nextActiveTabListItem.querySelector("a").click(); |
29 |
}
|
30 |
}
|
31 |
});
|
Conclusão
Parabéns, pessoal! Construímos este componente de guia JavaScript responsivo lindo e exclusivo sem escrever muito código. A partir daí, você pode usá-lo como está e torná-lo mais acessível verificando o código de um componente semelhante, como as guias do Bootstrap.
Alternativamente, você pode isolar o layout da lista de guias que se parece com um componente de passo e usá-lo como desejar, adicionando funcionalidade para setas de navegação, etc.
Antes de encerrar, vamos relembrar o que criamos hoje:
Como sempre, muito obrigado pela leitura!