Codelab ini mengajarkan cara membuat pengalaman seperti Instagram Stories di web. Kita akan membangun komponen sambil berjalan, dimulai dengan HTML, lalu CSS, kemudian JavaScript.
Lihat postingan blog saya Membangun komponen Stories untuk mempelajari peningkatan progresif yang dilakukan saat membangun komponen ini.
Penyiapan
- Klik Remix to Edit untuk membuat project dapat diedit.
- Buka
app/index.html
.
HTML
Saya selalu berupaya menggunakan HTML semantik.
Karena setiap teman dapat memiliki sejumlah cerita, saya pikir akan lebih baik menggunakan elemen
<section>
untuk setiap teman dan elemen <article>
untuk setiap cerita.
Namun, mari kita mulai dari awal. Pertama, kita memerlukan penampung untuk komponen
cerita.
Tambahkan elemen <div>
ke <body>
Anda:
<div class="stories">
</div>
Tambahkan beberapa elemen <section>
untuk merepresentasikan teman:
<div class="stories">
<section class="user"></section>
<section class="user"></section>
<section class="user"></section>
<section class="user"></section>
</div>
Tambahkan beberapa elemen <article>
untuk merepresentasikan cerita:
<div class="stories">
<section class="user">
<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>
<article class="story" style="--bg: url(https://picsum.photos/480/841);"></article>
</section>
<section class="user">
<article class="story" style="--bg: url(https://picsum.photos/481/840);"></article>
</section>
<section class="user">
<article class="story" style="--bg: url(https://picsum.photos/481/841);"></article>
</section>
<section class="user">
<article class="story" style="--bg: url(https://picsum.photos/482/840);"></article>
<article class="story" style="--bg: url(https://picsum.photos/482/843);"></article>
<article class="story" style="--bg: url(https://picsum.photos/482/844);"></article>
</section>
</div>
- Kami menggunakan layanan gambar (
picsum.com
) untuk membantu membuat prototipe cerita. - Atribut
style
pada setiap<article>
adalah bagian dari teknik pemuatan placeholder, yang akan Anda pelajari lebih lanjut di bagian berikutnya.
CSS
Konten kita siap untuk diberi gaya. Mari kita ubah kerangka tersebut menjadi sesuatu yang akan membuat orang ingin berinteraksi. Kami akan memprioritaskan situs seluler hari ini.
.stories
Untuk penampung <div class="stories">
, kita menginginkan penampung scrolling horizontal.
Kita dapat mencapainya dengan:
- Menjadikan penampung sebagai Petak
- Menyetel setiap anak untuk mengisi jalur baris
- Membuat lebar setiap turunan menjadi lebar area tampilan perangkat seluler
Grid akan terus menempatkan kolom lebar 100vw
baru di sebelah kanan kolom sebelumnya, hingga menempatkan semua elemen HTML dalam markup Anda.

Tambahkan CSS berikut ke bagian bawah app/css/index.css
:
.stories {
display: grid;
grid: 1fr / auto-flow 100%;
gap: 1ch;
}
Sekarang setelah kita memiliki konten yang melampaui area pandang, saatnya memberi tahu penampung tersebut cara menanganinya. Tambahkan baris kode yang ditandai ke set aturan .stories
Anda:
.stories {
display: grid;
grid: 1fr / auto-flow 100%;
gap: 1ch;
overflow-x: auto;
scroll-snap-type: x mandatory;
overscroll-behavior: contain;
touch-action: pan-x;
}
Kita menginginkan scroll horizontal, jadi kita akan menyetel overflow-x
ke
auto
. Saat pengguna men-scroll, kita ingin komponen berhenti dengan mulus di cerita berikutnya, jadi kita akan menggunakan scroll-snap-type: x mandatory
. Baca selengkapnya tentang CSS ini di bagian CSS Scroll Snap Points dan overscroll-behavior di postingan blog saya.
Penampung induk dan turunan harus menyetujui penempelan scroll, jadi
mari kita tangani sekarang. Tambahkan kode berikut ke bagian bawah app/css/index.css
:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
}
Aplikasi Anda belum berfungsi, tetapi video di bawah menunjukkan apa yang terjadi saat
scroll-snap-type
diaktifkan dan dinonaktifkan. Jika diaktifkan, setiap scroll horizontal akan berpindah ke artikel berikutnya. Jika dinonaktifkan, browser akan menggunakan perilaku scrolling default-nya.
Anda akan menelusuri teman, tetapi kami masih memiliki masalah dengan cerita yang harus diselesaikan.
.user
Mari kita buat tata letak di bagian .user
yang mengatur elemen story turunan tersebut. Kita akan menggunakan trik penumpukan praktis untuk menyelesaikannya.
Pada dasarnya, kita membuat petak 1x1 dengan baris dan kolom yang memiliki alias petak yang sama, yaitu [story]
, dan setiap item petak cerita akan mencoba mengklaim ruang tersebut, sehingga menghasilkan tumpukan.
Tambahkan kode yang ditandai ke set aturan .user
Anda:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
display: grid;
grid: [story] 1fr / [story] 1fr;
}
Tambahkan set aturan berikut ke bagian bawah app/css/index.css
:
.story {
grid-area: story;
}
Sekarang, tanpa pemosisian absolut, float, atau petunjuk tata letak lain yang mengeluarkan elemen dari alur, kita masih berada dalam alur. Selain itu, kodenya hampir tidak ada, lihat itu! Hal ini diuraikan dalam video dan postingan blog secara lebih mendetail.
.story
Sekarang kita hanya perlu menata gaya item cerita itu sendiri.
Sebelumnya, kami menyebutkan bahwa atribut style
pada setiap elemen <article>
adalah bagian dari
teknik pemuatan placeholder:
<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>
Kita akan menggunakan properti background-image
CSS, yang memungkinkan kita menentukan lebih dari satu gambar latar. Kita dapat mengurutkannya sehingga foto pengguna
berada di atas dan akan muncul secara otomatis saat selesai dimuat. Untuk
mengaktifkannya, kita akan memasukkan URL gambar ke dalam properti kustom (--bg
), dan menggunakannya
dalam CSS untuk menyusun dengan placeholder pemuatan.
Pertama, mari kita perbarui set aturan .story
untuk mengganti gradien dengan gambar latar belakang setelah selesai dimuat. Tambahkan kode yang ditandai ke set aturan .story
Anda:
.story {
grid-area: story;
background-size: cover;
background-image:
var(--bg),
linear-gradient(to top, lch(98 0 0), lch(90 0 0));
}
Menetapkan background-size
ke cover
memastikan tidak ada ruang kosong di
viewport karena gambar kita akan mengisinya. Menentukan 2 gambar latar
memungkinkan kita menarik trik web CSS yang rapi yang disebut tanda kuburan pemuatan:
- Gambar latar 1 (
var(--bg)
) adalah URL yang kami teruskan inline di HTML - Gambar latar 2 (
linear-gradient(to top, lch(98 0 0), lch(90 0 0))
adalah gradien yang ditampilkan saat URL sedang dimuat
CSS akan otomatis mengganti gradien dengan gambar, setelah gambar selesai didownload.
Selanjutnya, kita akan menambahkan beberapa CSS untuk menghapus beberapa perilaku, sehingga browser dapat bergerak lebih cepat.
Tambahkan kode yang ditandai ke set aturan .story
Anda:
.story {
grid-area: story;
background-size: cover;
background-image:
var(--bg),
linear-gradient(to top, lch(98 0 0), lch(90 0 0));
user-select: none;
touch-action: manipulation;
}
user-select: none
mencegah pengguna memilih teks secara tidak sengajatouch-action: manipulation
menginstruksikan browser bahwa interaksi ini harus diperlakukan sebagai peristiwa sentuh, yang membebaskan browser dari mencoba memutuskan apakah Anda mengklik URL atau tidak
Terakhir, mari tambahkan sedikit CSS untuk menganimasikan transisi antar-cerita. Tambahkan kode yang ditandai ke set aturan .story
Anda:
.story {
grid-area: story;
background-size: cover;
background-image:
var(--bg),
linear-gradient(to top, lch(98 0 0), lch(90 0 0));
user-select: none;
touch-action: manipulation;
transition: opacity .3s cubic-bezier(0.4, 0.0, 1, 1);
&.seen {
opacity: 0;
pointer-events: none;
}
}
Class .seen
akan ditambahkan ke cerita yang memerlukan pintu keluar.
Saya mendapatkan fungsi easing kustom (cubic-bezier(0.4, 0.0, 1,1)
)
dari panduan Easing Desain Material (scroll ke bagian Easing yang dipercepat).
Jika Anda jeli, Anda mungkin melihat deklarasi pointer-events: none
dan sekarang sedang berpikir keras. Saya rasa ini adalah satu-satunya kekurangan solusi ini sejauh ini. Kita memerlukan ini karena elemen .seen.story
akan berada di atas dan akan menerima ketukan, meskipun tidak terlihat. Dengan menyetel
pointer-events
ke none
, kita mengubah cerita kaca menjadi jendela, dan tidak lagi mencuri
interaksi pengguna. Tidak terlalu buruk, tidak terlalu sulit untuk dikelola di CSS kami saat ini. Kita tidak sedang menyulap z-index
. Saya masih merasa yakin dengan hal ini.
JavaScript
Interaksi komponen Cerita cukup sederhana bagi pengguna: ketuk di kanan untuk maju, ketuk di kiri untuk kembali. Hal-hal sederhana bagi pengguna cenderung sulit dikerjakan oleh developer. Namun, kami akan menangani sebagian besar prosesnya.
Penyiapan
Untuk memulai, mari kita hitung dan simpan sebanyak mungkin informasi yang kita bisa.
Tambahkan kode berikut ke app/js/index.js
:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
Baris pertama JavaScript mengambil dan menyimpan referensi ke root elemen HTML utama kita. Baris berikutnya menghitung posisi tengah elemen, sehingga kita dapat memutuskan apakah ketukan akan bergerak maju atau mundur.
Negara Bagian
Selanjutnya, kita membuat objek kecil dengan beberapa status yang relevan dengan logika kita. Dalam
kasus ini, kita hanya tertarik dengan cerita saat ini. Dalam markup HTML, kita dapat
mengaksesnya dengan mengambil teman pertama dan cerita terbarunya. Tambahkan kode yang disorot
ke app/js/index.js
Anda:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
const state = {
current_story: stories.firstElementChild.lastElementChild
}
Pemroses
Kita sekarang memiliki logika yang cukup untuk mulai memproses dan mengarahkan peristiwa pengguna.
Tikus
Mari kita mulai dengan memproses peristiwa 'click'
di penampung cerita.
Tambahkan kode yang ditandai ke app/js/index.js
:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
const state = {
current_story: stories.firstElementChild.lastElementChild
}
stories.addEventListener('click', e => {
if (e.target.nodeName !== 'ARTICLE')
return
navigateStories(
e.clientX > median
? 'next'
: 'prev')
})
Jika terjadi klik dan bukan pada elemen <article>
, kita akan membatalkan dan tidak melakukan apa pun.
Jika berupa artikel, kita mengambil posisi horizontal mouse atau jari dengan
clientX
. Kita belum menerapkan navigateStories
, tetapi argumen yang
diperlukan menentukan arah yang harus kita tuju. Jika posisi pengguna tersebut
lebih besar dari median, kita tahu bahwa kita harus menavigasi ke next
, jika tidak
prev
(sebelumnya).
Keyboard
Sekarang, mari kita dengarkan penekanan tombol keyboard. Jika Panah Bawah ditekan, kita akan membuka
next
. Jika tombolnya adalah Panah Atas, kita akan membuka prev
.
Tambahkan kode yang ditandai ke app/js/index.js
:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
const state = {
current_story: stories.firstElementChild.lastElementChild
}
stories.addEventListener('click', e => {
if (e.target.nodeName !== 'ARTICLE')
return
navigateStories(
e.clientX > median
? 'next'
: 'prev')
})
document.addEventListener('keydown', ({key}) => {
if (key !== 'ArrowDown' || key !== 'ArrowUp')
navigateStories(
key === 'ArrowDown'
? 'next'
: 'prev')
})
Navigasi Stories
Saatnya menangani logika bisnis unik cerita dan UX yang membuat cerita menjadi terkenal. Mungkin terlihat rumit, tetapi jika Anda membacanya baris demi baris, Anda akan mendapati bahwa panduan ini cukup mudah dipahami.
Di awal, kita menyimpan beberapa pemilih yang membantu kita memutuskan apakah akan men-scroll ke teman atau menampilkan/menyembunyikan story. Karena kita bekerja di HTML, kita akan mengkuerinya untuk mengetahui kehadiran teman (pengguna) atau cerita (story).
Variabel ini akan membantu kami menjawab pertanyaan seperti, "jika diberi cerita x, apakah "berikutnya" berarti beralih ke cerita lain dari teman yang sama atau ke teman yang berbeda?" Saya melakukannya dengan menggunakan struktur pohon yang kami buat, menjangkau induk dan turunannya.
Tambahkan kode berikut ke bagian bawah app/js/index.js
:
const navigateStories = direction => {
const story = state.current_story
const lastItemInUserStory = story.parentNode.firstElementChild
const firstItemInUserStory = story.parentNode.lastElementChild
const hasNextUserStory = story.parentElement.nextElementSibling
const hasPrevUserStory = story.parentElement.previousElementSibling
}
Berikut tujuan logika bisnis kita, yang sedekat mungkin dengan bahasa alami:
- Tentukan cara menangani ketukan
- Jika ada cerita berikutnya/sebelumnya: tampilkan cerita tersebut
- Jika itu adalah cerita terakhir/pertama teman: tampilkan teman baru
- Jika tidak ada cerita yang dapat dituju ke arah tersebut: jangan lakukan apa pun
- Simpan artikel saat ini yang baru ke
state
Tambahkan kode yang ditandai ke fungsi navigateStories
Anda:
const navigateStories = direction => {
const story = state.current_story
const lastItemInUserStory = story.parentNode.firstElementChild
const firstItemInUserStory = story.parentNode.lastElementChild
const hasNextUserStory = story.parentElement.nextElementSibling
const hasPrevUserStory = story.parentElement.previousElementSibling
if (direction === 'next') {
if (lastItemInUserStory === story && !hasNextUserStory)
return
else if (lastItemInUserStory === story && hasNextUserStory) {
state.current_story = story.parentElement.nextElementSibling.lastElementChild
story.parentElement.nextElementSibling.scrollIntoView({
behavior: 'smooth'
})
}
else {
story.classList.add('seen')
state.current_story = story.previousElementSibling
}
}
else if(direction === 'prev') {
if (firstItemInUserStory === story && !hasPrevUserStory)
return
else if (firstItemInUserStory === story && hasPrevUserStory) {
state.current_story = story.parentElement.previousElementSibling.firstElementChild
story.parentElement.previousElementSibling.scrollIntoView({
behavior: 'smooth'
})
}
else {
story.nextElementSibling.classList.remove('seen')
state.current_story = story.nextElementSibling
}
}
}
Cobalah
- Untuk melihat pratinjau situs, tekan Lihat Aplikasi. Kemudian, tekan
Layar Penuh
.
Kesimpulan
Itulah ringkasan kebutuhan saya dengan komponen tersebut. Jangan ragu untuk mengembangkannya, menggunakan data untuk menggerakkannya, dan menjadikannya milik Anda.