Como Construir um Efeito Hover Sublinhado com Desclocamento com CSS e JavaScript
Portuguese (Português) translation by Adriana Takagi (you can also view the original English article)
No tutorial de hoje, vamos usar um pouco de CSS e JavaScript para criar um efeito sofisticado de menu hover. Não é um resultado final complicado, mas construí-lo vai ser uma grande oportunidade para praticar nossas habilidades front-end.
Sem mais introdução, vamos conferir o que estaremos construindo:
A Linguagem de Marcação
Começamos com um marcador bem básico; um elemento nav
que contém o menu e um elemento vazio span
:
1 |
<nav class="mynav"> |
2 |
<ul>
|
3 |
<li>
|
4 |
<a href="">Home</a> |
5 |
</li>
|
6 |
<li>
|
7 |
<a href="">About</a> |
8 |
</li>
|
9 |
<li>
|
10 |
<a href="">Company</a> |
11 |
</li>
|
12 |
<li>
|
13 |
<a href="">Work</a> |
14 |
</li>
|
15 |
<li>
|
16 |
<a href="">Clients</a> |
17 |
</li>
|
18 |
<li>
|
19 |
<a href="">Contact</a> |
20 |
</li>
|
21 |
</ul>
|
22 |
</nav>
|
23 |
|
24 |
<span class="target"></span> |
O CSS
Com a linguagem de marcação pronta, em seguida, especificamos alguns estilos básicos para os elementos relacionados:
1 |
.mynav ul { |
2 |
display: flex; |
3 |
justify-content: center; |
4 |
flex-wrap: wrap; |
5 |
list-style-type: none; |
6 |
padding: 0; |
7 |
}
|
8 |
|
9 |
.mynav li:not(:last-child) { |
10 |
margin-right: 20px; |
11 |
}
|
12 |
|
13 |
.mynav a { |
14 |
display: block; |
15 |
font-size: 20px; |
16 |
color: black; |
17 |
text-decoration: none; |
18 |
padding: 7px 15px; |
19 |
}
|
20 |
|
21 |
.target { |
22 |
position: absolute; |
23 |
border-bottom: 4px solid transparent; |
24 |
z-index: -1; |
25 |
transform: translateX(-60px); |
26 |
}
|
27 |
|
28 |
.mynav a, |
29 |
.target { |
30 |
transition: all .35s ease-in-out; |
31 |
}
|
Note que o elemento span
(.target
) está absolutamente posicionado. Como vamos ver em um momento, vamos usar o JavaScript para determinar sua exata posição. Além disso, isso deve aparecer atrás dos links do menu, por isso atribua a ele um z-index
negativo.
O JavaScript
Neste ponto, vamos focar a nossa atenção no JavaScript necessário. Para começar, nós direcionamos nos elementos desejados. Nós também definimos um array de cores que vamos usar depois.
1 |
const target = document.querySelector(".target"); |
2 |
const links = document.querySelectorAll(".mynav a"); |
3 |
const colors = ["deepskyblue", "orange", "firebrick", "gold", "magenta", "black", "darkblue"]; |
Eventos
Em seguida prestamos atenção para os eventos click
e mouseenter
dos links do menu.
Quando o evento click
acontece, evitamos que a página seja recarregada. Claro, isso funciona no nosso caso porque todos os links têm um atributo vazio href
. Em um projeto real no entanto, cada um dos links do menu provavelmente abriria uma página diferente.
Mais importante ainda, assim que o evento mouseenter
dispara, a função de retorno mouseenterFunc
é executada:
1 |
for (let i = 0; i < links.length; i++) { |
2 |
links[i].addEventListener("click", (e) => e.preventDefault()); |
3 |
links[i].addEventListener("mouseenter", mouseenterFunc); |
4 |
}
|
mouseenterFunc
O corpo da função mouseenterFunc
tem esta aparência:
1 |
function mouseenterFunc() { |
2 |
for (let i = 0; i < links.length; i++) { |
3 |
if (links[i].parentNode.classList.contains("active")) { |
4 |
links[i].parentNode.classList.remove("active"); |
5 |
}
|
6 |
links[i].style.opacity = "0.25"; |
7 |
}
|
8 |
|
9 |
this.parentNode.classList.add("active"); |
10 |
this.style.opacity = "1"; |
11 |
|
12 |
const width = this.getBoundingClientRect().width; |
13 |
const height = this.getBoundingClientRect().height; |
14 |
const left = this.getBoundingClientRect().left; |
15 |
const top = this.getBoundingClientRect().top; |
16 |
const color = colors[Math.floor(Math.random() * colors.length)]; |
17 |
|
18 |
target.style.width = `${width}px`; |
19 |
target.style.height = `${height}px`; |
20 |
target.style.left = `${left}px`; |
21 |
target.style.top = `${top}px`; |
22 |
target.style.borderColor = color; |
23 |
target.style.transform = "none"; |
24 |
}
|
Dentro desta função fazemos o seguinte:
- Adicione a classe
active
ao pai imediato (li
) do link de destino. - Diminua a
opacity
de todos os links do menu, além do "active". - Use o método
getBoundingClientRect
para recuperar o tamanho do link associado e sua posição relativa à viewport. - Obtenha uma cor aleatória do array mencionado acima e passe-o como valor para a propriedade
border-color
do elementospan
. Lembre-se, seu valor de propriedade inicial é definido comotransparent
. - Atribua os valores extraídos do método
getBoundingClientRect
às propriedades correspondentes do elementospan
. Em outras palavras, a tagspan
herda o tamanho e a posição do link que está sendo passado o mouse. - Redefina a transformação padrão aplicada ao elemento
span
. Este comportamento é apenas importante na primeira vez que passamos o mouse sobre um link. Neste caso, a transformação do elemento passa detransform: translateX (-60px)
paratransform: none
. Isso nos dá um bom efeito de slide-in.
If Active
É importante observar que o código acima é executado toda vez que passamos o mouse sobre um link. Portanto ele é executado quando nós passamos o mouse sobre um link "ativo" também. Para evitar esse comportamento, nós acondicionamos o código acima dentro de uma instrução if
:
1 |
function mouseenterFunc() { |
2 |
if (!this.parentNode.classList.contains("active")) { |
3 |
// code here
|
4 |
}
|
5 |
}
|
Até agora, nossa demo tem a seguinte aparência:
Quase, mas Nem Tanto
Então, tudo parece funcionar como esperado, certo? Bem, isso não é verdade porque se percorrer a página ou redimensionar a janela de visualização, e depois tentar selecionar um link, as coisas ficam confusas. Especificamente, a posição do elemento span
torna-se incorreta.
Brinque com a demo da página inteira para ver o que quero dizer.
Para resolver isso, temos que calcular até que ponto percorremos a partir da parte superior da janela e adicionamos este valor ao valor atual top
do elemento de destino. Da mesma forma, devemos calcular até que ponto o documento tem sido rolado horizontalmente (apenas neste caso). O valor resultante é adicionado ao valor atual left
do elemento de destino.
Aqui estão as duas linhas de código que atualizamos:
1 |
const left = this.getBoundingClientRect().left + window.pageXOffset; |
2 |
const top = this.getBoundingClientRect().top + window.pageYOffset; |
Tenha em mente que todo o código acima é executado assim que o navegador processa o DOM e localiza o script relevante. Novamente, para suas próprias implementações e projetos você pode querer executar este código quando a página for carregada, ou algo parecido. Em tal cenário, você vai ter que incorporá-lo em um manipulador de eventos (por exemplo, manipulador de evento load
).
Viewport
A última coisa que temos a fazer é garantir que o efeito ainda vá funcionar à medida que redimensionamos a janela do navegador. Para fazer isto, nós prestamos atenção ao evento resize
e registramos o manipulador de eventos resizeFunc
.
1 |
window.addEventListener("resize", resizeFunc); |
Aqui está o corpo deste manipulador:
1 |
function resizeFunc() { |
2 |
const active = document.querySelector(".mynav li.active"); |
3 |
|
4 |
if (active) { |
5 |
const left = active.getBoundingClientRect().left + window.pageXOffset; |
6 |
const top = active.getBoundingClientRect().top + window.pageYOffset; |
7 |
|
8 |
target.style.left = `${left}px`; |
9 |
target.style.top = `${top}px`; |
10 |
}
|
11 |
}
|
Dentro da função acima, fazemos o seguinte:
- Verifique se há um item da lista de menu com a classe de
active
. Se existe tal elemento, isso indica que já passamos o mouse sobre um link. - Obtenha as novas propriedades
left
etop
do item "active" juntamente com as propriedades relacionadas da janela e atribua-as ao elementospan
. Observe que recuperamos os valores somente para as propriedades que mudam durante o eventoresize
. Isso significa que não há necessidade de recalcular a largura e a altura dos links do menu.
Suporte do Navegador
A demo funciona bem em todos os navegadores recentes. Se você encontrar quaisquer problemas, nos informe nos comentários abaixo. Além disso, como você possivelmente notou, usamos o Babel para compilar nosso código ES6 até ES5.
Conclusão
Nesta dica rápida, passamos pelo processo de criação de um simples, mas interessante efeito hover.
Espero que você tenha gostado do que construímos aqui e se inspirou para desenvolver efeitos de menu ainda mais poderosos como o que apareceu (no momento da escrita) no site Stripe.
Você já criou algo semelhante? Se for assim, não deixe de compartilhar conosco os desafios que enfrentou.