Codelab ini mengajarkan cara membangun pengalaman seperti Instagram Stories di web. Kita akan membuat komponen sambil berjalan, mulai dengan HTML, lalu CSS, lalu JavaScript.
Lihat postingan blog saya yang berjudul Membuat komponen Stories untuk mempelajari peningkatan progresif yang dilakukan saat membuat komponen ini.
Penyiapan
- Klik Remix untuk Mengedit agar project dapat diedit.
- Buka
app/index.html
.
HTML
Saya selalu berupaya menggunakan HTML semantik.
Karena setiap teman dapat memiliki berapa pun cerita, menurut saya akan bermanfaat jika menggunakan
elemen <section>
untuk setiap teman dan elemen <article>
untuk setiap cerita.
Mari kita mulai dari awal. Pertama, kita membutuhkan kontainer
untuk komponen {i>story<i}.
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 merepresentasikan artikel:
<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 bergaya. Mari kita ubah tulang-tulang itu menjadi sesuatu yang ingin berinteraksi dengan orang-orang. Hari ini kami akan bekerja dengan {i>mobile-first<i}.
.stories
Untuk penampung <div class="stories">
, kita menginginkan penampung scroll horizontal.
Kita dapat mencapainya dengan:
- Membuat container menjadi Grid
- Menetapkan setiap turunan untuk mengisi trek baris
- Membuat lebar setiap anak selebar area pandang perangkat seluler
Petak akan terus menempatkan kolom baru selebar 100vw
di sebelah kanan kolom sebelumnya, hingga semua elemen HTML di 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 ke luar area tampilan, saatnya memberi tahu container 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 diletakkan perlahan di cerita berikutnya,
jadi kita akan menggunakan scroll-snap-type: x mandatory
. Baca selengkapnya tentang CSS ini di bagian Titik Snap Scroll CSS
dan perilaku overscroll
di postingan blog saya.
Perlu penampung induk dan turunan untuk 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 mengarah ke cerita berikutnya. Jika dinonaktifkan, browser akan menggunakan
perilaku scroll default.
Ini akan membuat Anda menelusuri teman-teman Anda, tetapi kami masih memiliki masalah dengan cerita yang harus dipecahkan.
.user
Mari kita buat tata letak di bagian .user
yang menyusun elemen cerita turunan tersebut
pada tempatnya. Kami akan menggunakan trik tumpukan
yang praktis untuk menyelesaikan ini.
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 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 lain yang mengeluarkan elemen dari alur, kita masih dalam alur. Plus, ini seperti hampir semua kode, 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, kita menyebutkan bahwa atribut style
di 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 gambar
pengguna berada di atas dan akan muncul secara otomatis setelah pemuatan selesai. Untuk
mengaktifkannya, kita akan menempatkan URL gambar ke properti khusus (--bg
), dan menggunakannya
dalam CSS untuk memberi lapisan dengan placeholder pemuatan.
Pertama, mari kita perbarui kumpulan aturan .story
untuk mengganti gradien dengan gambar latar setelah pemuatan selesai. 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
akan memastikan tidak ada ruang kosong di area pandang karena gambar kita akan mengisinya. Dengan menentukan 2 gambar latar,
kita dapat menarik trik web CSS yang rapi yang disebut loading tombstone:
- Gambar latar 1 (
var(--bg)
) adalah URL yang kita 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 diunduh.
Selanjutnya kita akan menambahkan beberapa CSS untuk menghapus beberapa perilaku, sehingga browser bisa 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 mencoba memutuskan apakah Anda mengklik URL atau tidak
Terakhir, mari tambahkan sedikit CSS untuk menganimasikan transisi antarcerita. 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 jalan keluar.
Saya mendapatkan fungsi easing kustom (cubic-bezier(0.4, 0.0, 1,1)
) dari panduan Easing Desain Material (scroll ke bagian Accerlerated easing).
Jika Anda memiliki mata yang tajam, Anda mungkin memperhatikan deklarasi pointer-events: none
dan sedang menggaruk-garuk kepala. Saya akan mengatakan bahwa ini adalah
satu-satunya kelemahan dari 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 cerita kaca menjadi jendela, dan tidak mencuri
lagi interaksi pengguna. Tidak terlalu buruk, tidak terlalu sulit untuk
dikelola di CSS kami saat ini. Kami tidak melakukan juggling pada z-index
. Saya masih merasa
nyaman dengan ini.
JavaScript
Interaksi komponen Stories cukup sederhana bagi pengguna: ketuk kanan untuk melanjutkan, ketuk di sebelah kiri untuk kembali. Hal-hal sederhana bagi pengguna cenderung menjadi kerja keras bagi pengembang. Tapi kami akan menangani banyak hal ini.
Penyiapan
Untuk memulainya, mari kita hitung 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 kita mengambil dan menyimpan referensi ke root elemen HTML utama. Baris berikutnya menghitung di mana bagian 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. Dalam hal ini, kita hanya tertarik pada cerita saat ini. Dalam markup HTML, kita dapat mengaksesnya dengan
mengambil teman pertama dan kisah terbaru mereka. Tambahkan kode yang ditandai
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 memiliki logika yang cukup untuk mulai memproses peristiwa pengguna dan mengarahkannya.
Tikus
Mari kita mulai dengan mendengarkan 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 pada elemen <article>
, kami menjamin dan tidak akan melakukan apa pun.
Jika berupa artikel, kita ambil posisi horizontal mouse atau jari dengan
clientX
. Kita belum mengimplementasikan navigateStories
, tetapi argumen yang
diperlukan menentukan arah yang harus kita tempuh. Jika posisi pengguna tersebut
lebih besar dari median, kita tahu kita harus menuju ke next
, jika tidak,
prev
(sebelumnya).
Keyboard
Sekarang, mari kita dengarkan penekanan {i>keyboard<i}. Jika Panah Bawah ditekan, kita akan
membuka next
. Jika itu Panah Atas, kita mengarah ke 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 cerita
Saatnya menangani logika bisnis unik dari cerita dan UX yang membuatnya terkenal. Ini tampak rumit dan rumit, tetapi menurut saya, jika Anda mengambilnya baris demi baris, Anda akan merasa cukup mudah dicerna.
Di awal, kita menyimpan beberapa pemilih yang membantu memutuskan apakah akan men-scroll ke teman atau menampilkan/menyembunyikan cerita. Karena HTML adalah tempat kita bekerja, kita akan mengkuerinya untuk kehadiran teman (pengguna) atau cerita (cerita).
Variabel-variabel ini akan membantu kita menjawab pertanyaan seperti, "mengingat cerita x, apakah "berikutnya" berarti pindah ke cerita lain dari teman yang sama atau teman yang berbeda?" Saya melakukannya dengan menggunakan struktur pohon yang kami bangun, menjangkau orang tua dan anak mereka.
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 kami, semirip mungkin dengan natural language:
- Tentukan cara menangani ketukan
- Jika ada artikel berikutnya/sebelumnya: tampilkan cerita tersebut
- Jika itu cerita terakhir/pertama dari teman: perlihatkan teman baru
- Jika tidak ada kisah untuk diarahkan ke sana: jangan lakukan apa pun
- Simpan cerita baru saat ini 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. Lalu tekan Layar Penuh .
Kesimpulan
Itulah rangkuman dari kebutuhan yang saya miliki dengan komponen. Jangan ragu untuk membangunnya, menggerakkannya dengan data, dan secara umum membuatnya menjadi milik Anda.