Codelab ini mengajarkan cara membuat pengalaman seperti Instagram Stories di web. Kita akan mem-build komponen seiring berjalannya waktu, dimulai dengan HTML, lalu CSS, lalu JavaScript.
Lihat postingan blog saya Mem-build komponen Stories untuk mempelajari peningkatan progresif yang dilakukan saat mem-build komponen ini.
Penyiapan
- Klik Remix to Edit untuk menjadikan project dapat diedit.
- Buka
app/index.html
.
HTML
Saya selalu berusaha menggunakan HTML semantik.
Karena setiap teman dapat memiliki cerita dalam jumlah berapa pun, saya pikir akan lebih baik jika menggunakan
elemen <section>
untuk setiap teman dan elemen <article>
untuk setiap cerita.
Namun, mari kita mulai dari awal. Pertama, kita memerlukan penampung untuk
komponen story.
Tambahkan elemen <div>
ke <body>
Anda:
<div class="stories">
</div>
Tambahkan beberapa elemen <section>
untuk mewakili 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 mewakili 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>
- Kita menggunakan layanan gambar (
picsum.com
) untuk membantu membuat prototipe cerita. - Atribut
style
di setiap<article>
adalah bagian dari teknik pemuatan placeholder, yang akan Anda pelajari lebih lanjut di bagian berikutnya.
CSS
Konten kami siap untuk diberi gaya. Mari kita ubah tulang-tulang tersebut menjadi sesuatu yang ingin digunakan orang untuk berinteraksi. Hari ini kita akan mengerjakan versi seluler.
.stories
Untuk penampung <div class="stories">
, kita menginginkan penampung scroll horizontal.
Kita dapat melakukannya dengan:
- Membuat penampung menjadi Petak
- Menetapkan setiap turunan untuk mengisi jalur baris
- Membuat lebar setiap turunan menjadi lebar area pandang perangkat seluler
Petak akan terus menempatkan kolom baru dengan lebar 100vw
di sebelah kanan kolom sebelumnya, hingga semua elemen HTML dalam markup Anda ditempatkan.
Tambahkan CSS berikut ke bagian bawah app/css/index.css
:
.stories {
display: grid;
grid: 1fr / auto-flow 100%;
gap: 1ch;
}
Setelah kita memiliki konten yang meluas di luar area pandang, sekarang saatnya memberi tahu penampung
tersebut cara menanganinya. Tambahkan baris kode yang ditandai ke kumpulan 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 menetapkan overflow-x
ke
auto
. Saat pengguna men-scroll, kita ingin komponen beristirahat dengan lembut di story 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 snap 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 langsung beralih ke artikel berikutnya. Jika dinonaktifkan, browser akan menggunakan
perilaku scroll default-nya.
Tindakan ini akan membuat Anda men-scroll teman, tetapi kami masih memiliki masalah dengan artikel yang harus diselesaikan.
.user
Mari kita buat tata letak di bagian .user
yang mengatur elemen
cerita turunan tersebut. Kita akan menggunakan trik penumpukan yang praktis untuk mengatasinya.
Pada dasarnya, kita membuat petak 1x1 dengan baris dan kolom memiliki alias Grid
yang sama dengan [story]
, dan setiap item petak cerita akan mencoba mengklaim ruang tersebut,
sehingga menghasilkan stack.
Tambahkan kode yang ditandai ke kumpulan aturan .user
Anda:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
display: grid;
grid: [story] 1fr / [story] 1fr;
}
Tambahkan kumpulan aturan berikut ke bagian bawah app/css/index.css
:
.story {
grid-area: story;
}
Sekarang, tanpa pemosisian absolut, float, atau perintah tata letak lainnya yang membuat elemen keluar dari alur, kita masih dalam alur. Selain itu, hampir tidak ada kode, lihat itu! Hal ini dijelaskan secara lebih mendetail dalam video dan postingan blog.
.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 menyusunnya sehingga gambar pengguna
kita berada di atas dan akan muncul secara otomatis setelah selesai dimuat. Untuk
mengaktifkannya, kita akan memasukkan URL gambar ke dalam properti kustom (--bg
), dan menggunakannya
dalam CSS untuk ditumpuk dengan placeholder pemuatan.
Pertama, mari perbarui kumpulan aturan .story
untuk mengganti gradien dengan gambar latar belakang
setelah selesai dimuat. Tambahkan kode yang ditandai ke kumpulan 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
area pandang karena gambar kita akan mengisinya. Dengan menentukan 2 gambar latar, kita dapat menggunakan trik web CSS yang rapi yang disebut loading tombstone:
- Gambar latar 1 (
var(--bg)
) adalah URL yang kami teruskan secara inline di HTML - Gambar latar 2 (
linear-gradient(to top, lch(98 0 0), lch(90 0 0))
adalah gradien yang akan ditampilkan saat URL 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 kumpulan 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
memberi tahu browser bahwa interaksi ini harus diperlakukan sebagai peristiwa sentuh, yang membebaskan browser dari upaya untuk memutuskan apakah Anda mengklik URL atau tidak
Terakhir, mari kita tambahkan sedikit CSS untuk menganimasikan transisi antar-cerita. Tambahkan kode yang ditandai ke kumpulan 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 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 memiliki mata yang tajam, Anda mungkin melihat deklarasi pointer-events: none
dan sekarang sedang memutar otak. Saya rasa ini adalah satu-satunya
kelemahan solusi sejauh ini. Kita memerlukannya karena elemen .seen.story
akan berada di atas dan akan menerima ketukan, meskipun tidak terlihat. Dengan menetapkan
pointer-events
ke none
, kita mengubah story kaca menjadi jendela, dan tidak lagi
mencuri interaksi pengguna. Tidak terlalu buruk, tidak terlalu sulit dikelola di sini
di CSS kita saat ini. Kita tidak melakukan juggling z-index
. Saya masih merasa baik dengan hal ini.
JavaScript
Interaksi komponen Stories cukup sederhana bagi pengguna: ketuk di sebelah kanan untuk melanjutkan, ketuk di sebelah kiri untuk kembali. Hal-hal sederhana bagi pengguna cenderung menjadi pekerjaan berat bagi developer. Namun, kami akan menangani banyak hal.
Penyiapan
Untuk memulai, mari kita komputasi dan simpan informasi sebanyak mungkin.
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. Baris berikutnya menghitung letak tengah elemen, sehingga kita dapat memutuskan apakah ketukan akan maju atau mundur.
Negara Bagian
Selanjutnya, kita membuat objek kecil dengan beberapa status yang relevan dengan logika kita. Dalam
hal ini, kita hanya tertarik dengan cerita saat ini. Dalam markup HTML, kita dapat
mengaksesnya dengan mengambil teman pertama dan story terbarunya. 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
}
Pemroses
Sekarang kita memiliki logika yang cukup untuk mulai memproses peristiwa pengguna dan mengarahkannya.
Tikus
Mari kita mulai dengan memproses peristiwa 'click'
di penampung story.
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 klik terjadi dan tidak berada di elemen <article>
, kita akan keluar dan tidak melakukan apa pun.
Jika itu adalah artikel, kita mengambil posisi horizontal mouse atau jari dengan
clientX
. Kita belum menerapkan navigateStories
, tetapi argumen yang
dibutuhkan menentukan arah yang harus kita tuju. Jika posisi pengguna tersebut
lebih besar dari median, kita tahu bahwa kita harus membuka 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 membahas logika bisnis unik dari cerita dan UX yang membuat mereka terkenal. Ini terlihat tebal dan rumit, tetapi saya rasa jika Anda membacanya baris demi baris, Anda akan menemukan bahwa kode 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 HTML adalah tempat kita bekerja, kita akan mengirim kueri untuk mengetahui keberadaan teman (pengguna) atau cerita (story).
Variabel ini akan membantu kita menjawab pertanyaan seperti, "di cerita x, apakah "berikutnya" berarti beralih ke cerita lain dari teman yang sama ini atau ke teman yang berbeda?" Saya melakukannya dengan menggunakan struktur pohon yang kita buat, yang 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 adalah sasaran logika bisnis kita, sedekat mungkin dengan bahasa alami:
- Tentukan cara menangani ketukan
- Jika ada cerita berikutnya/sebelumnya: tampilkan cerita tersebut
- Jika ini adalah story terakhir/pertama dari 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 rangkuman kebutuhan yang saya miliki dengan komponen. Jangan ragu untuk mengembangkannya, mendorongnya dengan data, dan secara umum menjadikannya milik Anda.