Tantangan UI: Sliding Highlighter
Membuat animasi highlight yang bergeser ketika menu navigasi disorot
Di artikel ini
Spesifikasi
Antarmuka yang akan kita buat adalah highlighter untuk elemen menu yang sedang disorot. Berikut adalah spesifikasi lengkapnya:
- Apabila suatu elemen disorot, maka akan muncul highlighter di posisi elemen yang sedang disorot.
- Apabila kemudian menyorot elemen lainnya, maka highlighter akan bergeser ke elemen yang baru disorot.
- Lebar highlighter menyesuaikan elemen yang sedang disorot.
Berikut adalah demo dari antarmuka yang akan kita buat. Interaksi pada demo ini hanya bisa dilakukan dengan pointing device (misalnya mouse). Apabila kamu membuka halaman ini dengan perangkat yang tidak mempunyai pointing device, kamu bisa menonton video demo yang tercantum di bawahnya.
Video
Ide Dasar
Ide dasarnya adalah dengan menambahkan elemen yang berfungsi sebagai highlighter untuk kemudian melakukan transisi perubahan nilai dari properti opacity, transform, dan width. Berikut adalah rinciannya.
- Awalnya, nilai
opacitydari highlighter adalah 0. Ketika disorot, nilaiopacity-nya berubah menjadi 1. - Beri
position: absolute;pada highlighter - Atur posisi (relatif terhadap container)-nya mengunakan
transform: translateX(x);. Nilaixtersebut adalah nilaioffsetLeftdari elemen yang sedang disorot. - Atur lebarnya berdasarkan nilai
offsetWidthdari elemen yang sedang disorot.
Langkah 1: Markup dan Styling Dasar
<nav class="nav"> <!-- Highlighter --> <div class="highlighter" aria-hidden="true"></div>
<!-- Navigation items --> <a href="#overview" class="nav-item">Overview</a> <a href="#installation" class="nav-item">Installation</a> <a href="#faqs" class="nav-item">FAQs</a> <a href="#support" class="nav-item">Support</a></nav>.nav { position: relative; display: flex; overflow: auto; background: white; border-radius: 99999px;}
.nav-item { padding: 0.5rem 1rem; text-decoration: none; cursor: pointer;}
.highlighter { position: absolute; left: 0; height: 100%; background-color: #fedf89; border-radius: 99999px;}Langkah 2: Menambahkan mouseover Listener
Untuk setiap elemen menu, kita tambahkan mouseover listener. Hal yang dilakukan adalah mengatur posisi highlighter dengan menambahkan inline style transform: translateX(...) yang nilainya disesuaikan dengan nilai offsetLeft dari elemen menu yang disorot. Selain itu, kita juga perlu mengatur lebar highlighter untuk disesuaikan dengan nilai offsetWidth dari elemen menu yang disorot melalui penambahan inline style width.
const highlighter = document.querySelector('nav > .highlighter');const navItems = document.querySelectorAll('nav > .nav-item');
for (const navItem of navItems) { navItem.addEventListener('mouseover', (e) => { if (highlighter && e.currentTarget) { highlighter.style.width = `${e.currentTarget.offsetWidth}px`; highlighter.style.transform = `translateX(${e.currentTarget.offsetLeft}px)`; } });}Langkah 3: Menangani Sorotan Pertama
Sampai titik ini, spesifikasi nomor 2 dan 3 sudah kita penuhi. Namun, apabila kita menyorot keluar dari area elemen container (.nav) dan kemudian menyorot kembali ke salah satu elemen menu, tinjau bahwa highlighter akan muncul di elemen yang sebelumnya disorot dan kemudian bergeser ke elemen yang baru disorot. Efek tersebut tidak sesuai dengan spesifikasi nomor 1.
Untuk mengatasinya, kita harus membedakan perilaku apabila disorot untuk pertama kali dan disorot pada interaksi selanjutnya (bergeser):
- Tambahkan variabel
entereduntuk menyimpan status sorotan. Beri nilai awalanfalseyang artinya belum pernah menyorot apapun. - Apabila suatu elemen menu disorot pertama kali, maka transisi hanya dilakukan untuk properti
opacitydan ubah nilaienteredmenjaditrue. - Untuk interaksi sorotan selanjutnya, lakukan transisi untuk properti
opacity,transform, danwidth. - Apabila sorotan keluar dari area elemen
.nav, ubah nilaienteredmenjadifalsekembali.
const highlighter = document.querySelector('.nav > .highlighter');const navItems = document.querySelectorAll('.nav > .nav-item');
let entered = false;
for (const navItem of navItems) { navItem.addEventListener('mouseover', (e) => { if (highlighter && e.currentTarget) { highlighter.style.width = `${e.currentTarget.offsetWidth}px`; highlighter.style.transform = `translateX(${e.currentTarget.offsetLeft}px)`;
if (entered == true) { highlighter.style.transitionProperty = 'width, transform, opacity'; } else { highlighter.style.transitionProperty = 'opacity'; entered = true; } } });}
document.querySelector('.nav')?.addEventListener('mouseleave', (e) => { entered = false;});