1. Web Design
  2. UX/UI
  3. Responsive Design

Membuat Timeline Horizontal dengan CSS dan JavaScript

Scroll to top

() translation by (you can also view the original English article)

Di kiriman sebelumnya, saya menunjukkan cara membuat sebuah linimasa responsif vertikal dari awal. Hari ini, saya akan melingkupi proses dari pembuatan lini masa horizontal.

Seperti biasa, untuk menangkap ide awal mengenai apa yang akan kita buat. Lihatlah sebentar ke demo CodePen (cek versi yang lebih besar untuk pengalaman yang lebih baik)

Ada banyak hal yang akan kita bahas, jadi mari kita mulai!

1. HTML Markup

Markup-nya adalah markup yang mirip dengan yang kita definisikan untuk linimasa vertikal, terbagi dari tiga hal kecil:

  • Kita menggunakan ordered list ketimbang sebuah unordered list karena lebih tepat secara semantik.
  • Ada sebuah list item ekstra (yang terakhir) yang kosong. Di bagian berikutnya, kita akan menjelaskan alasannya.
  • Ada sebuah elemen ekstra (yaitu .arrows) yang bertanggung jawab untuk navigasi di linimasa.

Ini adalah markup yang diperlukan:

1
<section class="timeline">
2
  <ol>
3
    <li>
4
      <div>
5
        <time>1934</time>
6
        Some content here
7
      </div>
8
    </li>
9
     
10
    <!-- more list items here -->
11
    
12
    <li></li>
13
  </ol>
14
  
15
  <div class="arrows">
16
    <button class="arrow arrow__prev disabled" disabled>
17
      <img src="arrow_prev.svg" alt="prev timeline arrow">
18
    </button>
19
    <button class="arrow arrow__next">
20
      <img src="arrow_next.svg" alt="next timeline arrow">
21
    </button>
22
  </div>
23
</section>

Keadaan awal dari linimasa akan tampak seperti ini:

2. Menambahkkan Initial CSS Styles

Setelah beberapa style font dasar, color style dan lainnya. Yang telah saya hilangkan di sini demi kesederhanaan. Kita mengspesifikasikan beberapa structural CSS rules:

1
.timeline {
2
  white-space: nowrap;
3
  overflow-x: hidden;
4
}
5
6
.timeline ol {
7
  font-size: 0;
8
  width: 100vw;
9
  padding: 250px 0;
10
  transition: all 1s;
11
}
12
13
.timeline ol li {
14
  position: relative;
15
  display: inline-block;
16
  list-style-type: none;
17
  width: 160px;
18
  height: 3px;
19
  background: #fff;
20
}
21
22
.timeline ol li:last-child {
23
  width: 280px;
24
}
25
26
.timeline ol li:not(:first-child) {
27
  margin-left: 14px;
28
}
29
30
.timeline ol li:not(:last-child)::after {
31
  content: '';
32
  position: absolute;
33
  top: 50%;
34
  left: calc(100% + 1px);
35
  bottom: 0;
36
  width: 12px;
37
  height: 12px;
38
  transform: translateY(-50%);
39
  border-radius: 50%;
40
  background: #F45B69;
41
}

Yang paling penting di sini, kamu akan menyadari dua hal:

  • Kita meng-assign top dan bottom padding ke list. Sekali lagi, kita akan jelaskan kenapa ini terjadi di bagian berikutnya.
  • Seperti yang kamu sadari di demo di bawah, di titik ini kita tidak bisa melihat semua item karena list-nya memiliki width: 100vw dengan parent overflow-x: hidden. Ini secara efektif akan meng-mask item di list. Terima kasih kepada navigasi linimasa. Namun, kita akan bisa bernavigasi ke item-itemnya nanti.

Dengan aturan ini di tempat, ini adalah keadaan saat ini dari linimasa (tanpa konten aktual apapun, untuk membuatnya jelas):

3. Elemen Styles Linimasa

Hingga titik ini kita telah mengatur elemen div (kita akan sebut mereka "elemen linimasa" mulai sekarang) yang merupakan bagian dari list item karena elemen semu ::before mereka.

Tambahan, kita akan menggunakan class CSS semu :nth-child(odd) dan :nth-child(even) untuk membedakan gaya dari div ganjil dan genap.

Ini adalah gaya umum untuk elemen linimasa:

1
.timeline ol li div {
2
  position: absolute;
3
  left: calc(100% + 7px);
4
  width: 280px;
5
  padding: 15px;
6
  font-size: 1rem;
7
  white-space: normal;
8
  color: black;
9
  background: white;
10
}
11
12
.timeline ol li div::before {
13
  content: '';
14
  position: absolute;
15
  top: 100%;
16
  left: 0;
17
  width: 0;
18
  height: 0;
19
  border-style: solid;
20
}

Lalu, beberapa styles untuk yang ganjil:

1
.timeline ol li:nth-child(odd) div {
2
  top: -16px;
3
  transform: translateY(-100%);
4
}
5
6
.timeline ol li:nth-child(odd) div::before {
7
  top: 100%;
8
  border-width: 8px 8px 0 0;
9
  border-color: white transparent transparent transparent;
10
}

Dan terakhir beberapa styles untuk yang genap:

1
.timeline ol li:nth-child(even) div {
2
  top: calc(100% + 16px);
3
}
4
5
.timeline ol li:nth-child(even) div::before {
6
  top: -8px;
7
  border-width: 8px 0 0 8px;
8
  border-color: transparent transparent transparent white;
9
}

Ini adalah tampilan baru dari linimasa dengan konten dimasukkan:

Seperti yang mungkin kamu sadari, elemen linimasi terposisikan secara absolut. Ini artinya mereka dihapus dari alur dokumen normal. Dengan hal tersebut diingat, untuk memastikan agar seluruh linimasa dapat tampil, kita harus menyiapkan nilai padding atas dan bawah yang bear untuk list-nya. Jika kita tidak menerapkan padding apapun, maka linimasanya akan terpotong.

How the timeline looks like without paddingsHow the timeline looks like without paddingsHow the timeline looks like without paddings

4. Styles Navigasi Linimasa

Sekarang waktunya untuk memberi style dari tombol navigasi. Ingat bahwa pada dasarnya kita menonaktifkan panah kembali dan memberikannya kelas disabled.

Ini adalah CSS Styles yang terasosiasi:

1
.timeline .arrows {
2
  display: flex;
3
  justify-content: center;
4
  margin-bottom: 20px;
5
}
6
7
.timeline .arrows .arrow__prev {
8
  margin-right: 20px;
9
}
10
11
.timeline .disabled {
12
  opacity: .5;
13
}
14
15
.timeline .arrows img {
16
  width: 45px;
17
  height: 45px;
18
}

Aturan di atas memberikan kita linimasa berikut:

5. Menambahkan Interaktifitas

Struktur dasar dari linimasa telah siap. Mari tambahkan beberapa interaktifitas ke dalamnya!

Variabel

Hal yang pertama, kita menyiapkan sekumpulan variabel yang akan kita gunakan nanti.

1
const timeline = document.querySelector(".timeline ol"),
2
  elH = document.querySelectorAll(".timeline li > div"),
3
  arrows = document.querySelectorAll(".timeline .arrows .arrow"),
4
  arrowPrev = document.querySelector(".timeline .arrows .arrow__prev"),
5
  arrowNext = document.querySelector(".timeline .arrows .arrow__next"),
6
  firstItem = document.querySelector(".timeline li:first-child"),
7
  lastItem = document.querySelector(".timeline li:last-child"),
8
  xScrolling = 280,
9
  disabledClass = "disabled";

Menginisialisasi

Ketika semua aset halaman telah siap, fungsi init akan terpanggil.

1
window.addEventListener("load", init);

Fungsi ini akan memicu empat sub-functions:

1
function init() {
2
  setEqualHeights(elH);
3
  animateTl(xScrolling, arrows, timeline);
4
  setSwipeFn(timeline, arrowPrev, arrowNext);
5
  setKeyboardFn(arrowPrev, arrowNext);
6
}

Seperti yang kita lihat, setiap fungsi menyelesaikan tugas tertentu.

Elemen Equal-Height Linimasa

Jika kamu kembali ke demo terakhir kamu akan menyadari bahwa elemen linimasa tidak memiliki tinggi yang sama. Ini tidak akan mempengaruhi fungsi utama dari linimasa kita. Tapi, jika kamu mungkin ingin setiap elemen linimasa memiliki tinggi yang sama. Untuk mencapainya, kita bisa memberikan mereka baik sebuah tinggi fixed melalui CSS (solusi mudah) atau tinggi dinamis yang berkorenspon dengan elemen tertinggi melaui JavaScript.

Opsi kedua jauh lebih fleksibel dan stabil, jadi inilah sebuah fungsi yang mengimplementasikannya.

1
function setEqualHeights(el) {
2
  let counter = 0;
3
  for (let i = 0; i < el.length; i++) {
4
    const singleHeight = el[i].offsetHeight;
5
    
6
    if (counter < singleHeight) {
7
      counter = singleHeight;
8
    }
9
  }
10
  
11
  for (let i = 0; i < el.length; i++) {
12
    el[i].style.height = `${counter}px`;
13
  }
14
}

Fungsi ini akan memberikan tinggi dari elemen tertinggi di linimana dan mengaturnya sebagai dasar dari tinggi untuk semua elemen.

Ini adalah tampilan demonya:

6. Menganimasikan Linimasa

Sekarang mari fokus pada animasi di linimasa, kita akan membuat fungsi yang akan mengimplementasikannya.

Pertama, mari daftarkan sebuah click event listener untuk tombol linimasa:

1
function animateTl(scrolling, el, tl) {
2
  for (let i = 0; i < el.length; i++) {
3
    el[i].addEventListener("click", function() {
4
      // code here

5
    });
6
  }
7
}

Setiap kali tombol di klik, kita mencentang keadaan nonakftif dari tombol linimasa dan jika mereka tidak nonaktif kita akan menonaktifkannya. Ini memastikan bahwa kedua tombol hanya akan di klik sekali hingga animasi selesai.

Jadi, dalam hal kode, click handler akan mengandung baris berikut:

1
if (!arrowPrev.disabled) {
2
  arrowPrev.disabled = true;
3
}
4
5
if (!arrowNext.disabled) {
6
  arrowNext.disabled = true;
7
}

Langkah selanjutnya adalah sebagai berikut:

  • Kita akan mengecek untuk melihat apakah ini waktu pertama kita mengklik sebuah tombol. Sekali lagi, ingiat bahwa tombol previous pada dasarnya diatur ke disabled, jadi tombol yang bisa diklik hanya tombol next.
  • Kalau memang ini kali yang pertama, kita menggunakan properti transform untuk memindahkan linimasa 280px ke kanan. Nilai dari variable xScrolling menentukan jumlah dari pergerakan.
  • Di sisi lain, jika kita telah mengklik sebuah tombol, kita akan mendapatkan nilai dari transform saat ini dan menambahkan atua mengurangi nilainya sesuai jumlah yang diinginkan (yaitu 280px). Jadi, sepanjang kita mengklik tombol previous, nilai properti transform akan berukurang dan linimasa bergerak ke kiri. Namun, ketika tombol next diklik, nilai dari properti transform meningkat dan linimasa bergerak di kanan ke kiri.

Kode yang mengimplementasikannya sebagai berikut:

1
let counter = 0; 
2
for (let i = 0; i < el.length; i++) {
3
  el[i].addEventListener("click", function() {
4
    // other code here

5
  
6
    const sign = (this.classList.contains("arrow__prev")) ? "" : "-";
7
    if (counter === 0) {
8
      tl.style.transform = `translateX(-${scrolling}px)`;
9
    } else {
10
      const tlStyle = getComputedStyle(tl);
11
      // add more browser prefixes if needed here

12
      const tlTransform = tlStyle.getPropertyValue("-webkit-transform") || tlStyle.getPropertyValue("transform");
13
      const values = parseInt(tlTransform.split(",")[4]) + parseInt(`${sign}${scrolling}`);
14
      tl.style.transform = `translateX(${values}px)`;
15
    }
16
    counter++;
17
  });
18
}

Kerja bagus! Kita baru saja mendefinisikan sebuah cara menganimasikan linimasa. Tantang selanjutnya adalah untuk mencari tahu kapan animasi ini harus berhenti. Begini pendekatan kami:

  • Ketika elemen pertama linimasa jadi dapat terlihat sepenuhnya, ini berarti kitatelah mencapai awal dari linimasa, dan akan menonaktifkan tombol previous. Kita juga memastikan tombol next aktif.
  • Ketika elemen terakhir dari linimasa terlihatsepenuhnya, ini berarti kita telah mencapai akhir dari linimasa, sehingga kita menonaktifkan tombol next. Kita juga memastika tombol previous aktif.

Ingat bahwa element terakhir adalah yang kosong dengan lebar yang sama dengan elemen linimasa (yaitu 280px). Kita memberikan nilai ini (atau yang lebih tinggi) karena kita ingin memastikan bahwa leemen terkahir linimana akan terlihat sebelum mematikan tombol next.

Untuk mendeteksi elemen target telah terliha sepenuhnya di viewport atau tidak. Kita akan mengambil keuntungan dari kode yang kita gunakan di linimasa vertikal. Kode yang dibutuhkan muncul dari Thread Stack Overflow seperti berikut:

1
function isElementInViewport(el) {
2
  const rect = el.getBoundingClientRect();
3
  return (
4
    rect.top >= 0 &&
5
    rect.left >= 0 &&
6
    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
7
    rect.right <= (window.innerWidth || document.documentElement.clientWidth)
8
  );
9
}

Disamping fungsi di atas, kita akan mendefinisikan pembantu lainnya:

1
function setBtnState(el, flag = true) {
2
  if (flag) {
3
    el.classList.add(disabledClass);
4
  } else {
5
    if (el.classList.contains(disabledClass)) {
6
      el.classList.remove(disabledClass);
7
    }
8
    el.disabled = false;
9
  }
10
}

Fungsi ini menambahkan atau menghilangkan class disabled dari sebuah elemen berdasarkan nilai dari parameter flag. Dia bisa merubah keadaan disabled untuk elemen ini.

Seletah memberikan deskripsi di atas, ini adalah kode yang kita buat untuk mengecek apakah animasi harus berhenti ataukah tidak:

1
for (let i = 0; i < el.length; i++) {
2
  el[i].addEventListener("click", function() {
3
    // other code here

4
    
5
    // code for stopping the animation

6
    setTimeout(() => {
7
      isElementInViewport(firstItem) ? setBtnState(arrowPrev) : setBtnState(arrowPrev, false);
8
      isElementInViewport(lastItem) ? setBtnState(arrowNext) : setBtnState(arrowNext, false);
9
    }, 1100);
10
  
11
    // other code here

12
  });
13
}

Perhatikan ada delay 1.1 detik sebelum mengeksekusi kode ini. Kenapa ini terjadi?

Jika kita kembali ke CSS kita, kita akan melihat rule berikut:

1
.timeline ol {
2
  transition: all 1s;
3
}

Jadi, animasi linimasa membutuhkan waktu satu detik untuk selesai. Dan saat selesai kita akan menunggu 100 milisekon lalu melakukan cek.

Beginilah linimasa dengan aimasi:

7. Menambahkan Dukungan Geser

Sejauh ini, linimasa tidak merespon event sentuhan. Ini akan jadi bagus jika kita bisa menambahkan fungsi tersebut. Untuk menyelesaikannya, kita bisa menulis kode JavaScript untuk implementasinya atau menggunakan salah satu pustaka terkait (seperti Hammer.js, TouchSwipe.js) yang ada di luar sana.

Untuk demo kita, kita akan membuatnya tetap sederhana dan menggunakan Hammer.js, jadi pertama-tama kita memasukkan pustakanya ke pen kita.

How to include Hammerjs in our penHow to include Hammerjs in our penHow to include Hammerjs in our pen

Lalu kita deklarasikan fungsi yang terasosiasi:

1
function setSwipeFn(tl, prev, next) {
2
  const hammer = new Hammer(tl);
3
  hammer.on("swipeleft", () => next.click());
4
  hammer.on("swiperight", () => prev.click());
5
}

Di dalam fungsi di atas kita melakukan hal berikut:

  • Membuat instance dari Hammer.
  • Mendaftarkan handler untuk event swipeleft dan swiperight.
  •  Ketika menggeser linimasa ke arah kiri, kita memicu sebuah klik ke tombol next dan menggeser linimasa dari kanan ke kiri.
  • Ketika kita menggeser linimasa ke arah kanan, kita memicu sebuah klik ke tombol previous dan akan menganimasikan linimasa dari kiri ke kanan.

Linimasa dengan dukungan layar sentuh:

Menambahkan Navigasi Keyboard

Mari tingkatkan lebih jauh pengalaman pengguna dengan menyediakan dukungan navigasi papan ketik, tujuan kita:

  • Ketika panah kiri atau kanan ditekan, dokumen harus tergeser ke posisi atas dari lini masa (jika bagian halaman lain sedang terlihat). Ini memastkan seluruh linimasa akan terlihat.
  • Secara spesifik, ketika tombol kiri ditekan, linimasa harus teranimasi dari kiri ke kanan.
  • Di cara yang sama, ketika tombol kanan ditekan, lini masa harus beranimasi dari kanan ke kiri.

Fungsi yang terasosiasi adalah sebagai berikut:

1
function setKeyboardFn(prev, next) {
2
  document.addEventListener("keydown", (e) => {
3
    if ((e.which === 37) || (e.which === 39)) {
4
      const timelineOfTop = timeline.offsetTop;
5
      const y = window.pageYOffset;
6
      if (timelineOfTop !== y) {
7
        window.scrollTo(0, timelineOfTop);
8
      } 
9
      if (e.which === 37) {
10
        prev.click();
11
      } else if (e.which === 39) {
12
        next.click();
13
      }
14
    }
15
  });
16
}

Linimasa dengan dukungan papan ketik:

8. Menjadi responsif

Kita hampir selesai! Namun sebelumnya, mari kita buat linimasanya menjadi responsif. Ketika viewport-nya kurang dari 600px, ini seharusnya memiliki layout seperti berikut:

Karena kita menggunakan pendekatan desktop, ini adalah CSS rules yang harus kita tulis lagi.

1
@media screen and (max-width: 599px) {
2
  .timeline ol,
3
  .timeline ol li {
4
    width: auto; 
5
  }
6
  
7
  .timeline ol {
8
    padding: 0;
9
    transform: none !important;
10
  }
11
  
12
  .timeline ol li {
13
    display: block;
14
    height: auto;
15
    background: transparent;
16
  }
17
  
18
  .timeline ol li:first-child {
19
    margin-top: 25px;
20
  }
21
  
22
  .timeline ol li:not(:first-child) {
23
    margin-left: auto;
24
  }
25
  
26
  .timeline ol li div {
27
    width: 94%;
28
    height: auto !important;
29
    margin: 0 auto 25px;
30
  }
31
  
32
  .timeline ol li:nth-child div {
33
    position: static;
34
  }
35
  
36
  .timeline ol li:nth-child(odd) div {
37
    transform: none;
38
  }
39
  
40
  .timeline ol li:nth-child(odd) div::before,
41
  .timeline ol li:nth-child(even) div::before {
42
    left: 50%;
43
    top: 100%;
44
    transform: translateX(-50%);
45
    border: none;
46
    border-left: 1px solid white;
47
    height: 25px;
48
  }
49
  
50
  .timeline ol li:last-child,
51
  .timeline ol li:nth-last-child(2) div::before,
52
  .timeline ol li:not(:last-child)::after,
53
  .timeline .arrows {
54
    display: none;
55
  }
56
}

Catatan: Untuk kedua aturan di atas, kita telah menggunakan aturan !important untuk inline styles yang diterapkan melalui JavaScript.

Keadaan akhir dari linimasa kita:

Dukungan Browser

Demo tersebut bekerja dengan baik di semua perangkat dan browser terbaru. Namun yang mungkin kamu sadari kita menggunakan Babel untuk meng-compile kode ES6 kita ke ES5.

Satu-satunya isu kecil yang saya alami ketika mengetes adalah perubahan teks render yang terjadi ketika linimasa sedang dianimasikan. Meskipun saya telah mencoba berbagai pendekatan berbeda yang ditawarkan Stack Overflow, saya tidak menemukan solusi mudah untuk semua sistem operasi dan browser. Jadi, simpan di pikiranmu bahwa kamu mungkin melihat sedikit masalah font rendering saat linimasa sedang dianimasikan.

Kesimpulan

Di panduan penting ini, kita memulai dengan ordered list sederhana dan membuat sebuah linimasa horizontal responsif. Tanpa ragu, kita telah membahas banyak hal menarik dan saya harap kamu menikmati pekerjaan hingga hasil akhir dan ini membantumu mendapatkan pengetahuan baru.

Jika kamu memiliki pertanyaan atau sesuatu yang tidak kamu pahami, biarkan saya tahu di kolom komentar di bawah!

Langkah Berikutnya

Jika kamu ingin mengembangkan lebih linimasa, ini adalah beberapa hal yang bisa dilakukan:

  • Menambahkan dukungan dragging. Daripada mengklik tombol di linimasa untuk bernavigasi, kita bisa cukup men-drag area linimasa. Untuknya, kamu bisa baik menggunakan Drag and Drop API (yang sayangnya tidak mendukung perangkat mobile saat ini ditulis) atau pustaka eksternal seperti Draggable.js
  • Tingkatkan perilaku linimasa seperti saat kita mengecilkan layar di mana tombol harus aktif dan nonaktif dengan benar.
  • Atur kode dalam cara yang lebih mudah diatur, mungkin, menggunakan JavaScript Design Pattern.
Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Web Design tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.